feat(frontend): add unified header and footer to public pages
Implemented consistent navigation across all public-facing pages with a reusable layout system. Created PublicLayout component that wraps pages with a header containing the logo and a footer with navigation links. Changes: - Created PublicHeader component with logo linking to homepage - Created PublicFooter component with Product, Account, and Support sections - Created PublicLayout wrapper component using flex layout - Updated all public pages to use PublicLayout: - LoginPage, RegisterPage, ForgotPasswordPage, ResetPasswordPage - VerifyEmailPage, ContactPage, AboutUsPage, HowItWorksPage - NotFoundPage - Fixed gradient background pages to use min-h-full for proper height - Fixed content pages to avoid min-h-screen conflicts with flex-grow - Updated About Us content
This commit is contained in:
@@ -1,26 +1,9 @@
|
||||
# About Us
|
||||
Hi, I’m Radek – a software engineer, a West Coast Swing dancer and the person behind **spotlight.cam**.
|
||||
|
||||
## Lorem Ipsum
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
Spotlight.cam is a project built by someone who actually stands in the same registration lines, dances in the same heats and scrolls through the same event pages as you :P
|
||||
|
||||
## Dolor Sit Amet
|
||||
|
||||
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat:
|
||||
If we ever meet at an event, I’ll probably be somewhere near the dance floor, probably pressing “record” on someone’s spotlight.
|
||||
|
||||
- **Duis aute irure** - Dolor in reprehenderit in voluptate velit esse cillum dolore
|
||||
- **Eu fugiat nulla** - Pariatur excepteur sint occaecat cupidatat non proident
|
||||
- **Sunt in culpa** - Qui officia deserunt mollit anim id est laborum
|
||||
- **Sed ut perspiciatis** - Unde omnis iste natus error sit voluptatem
|
||||
|
||||
## Consectetur Adipiscing
|
||||
|
||||
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.
|
||||
|
||||
## Nemo Enim
|
||||
|
||||
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.
|
||||
|
||||
---
|
||||
|
||||
*For questions or feedback, [contact us](/contact).*
|
||||
To be fair, the original idea for this service was actually dropped on me by a friend who was tired of hunting for someone to film her dances – I just did what any backend developer / DevOps / Linux admin would do: said “okay, that shouldn’t be that hard… right?”, opened my editor, set up a few servers and scripts… and suddenly we had a new project on our hands 😅 I also had a not-so-secret teammate: AI. Without it, this would probably still be stuck in my “one day” folder for about a year 😄
|
||||
71
frontend/src/components/layout/PublicFooter.jsx
Normal file
71
frontend/src/components/layout/PublicFooter.jsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const PublicFooter = () => {
|
||||
return (
|
||||
<footer className="bg-white border-t border-gray-200 mt-auto">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{/* Product */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-900 uppercase tracking-wider mb-4">
|
||||
Product
|
||||
</h3>
|
||||
<ul className="space-y-2">
|
||||
<li>
|
||||
<Link to="/about-us" className="text-gray-600 hover:text-primary-600 transition-colors">
|
||||
About Us
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/how-it-works" className="text-gray-600 hover:text-primary-600 transition-colors">
|
||||
How It Works
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Account */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-900 uppercase tracking-wider mb-4">
|
||||
Account
|
||||
</h3>
|
||||
<ul className="space-y-2">
|
||||
<li>
|
||||
<Link to="/login" className="text-gray-600 hover:text-primary-600 transition-colors">
|
||||
Sign In
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/register" className="text-gray-600 hover:text-primary-600 transition-colors">
|
||||
Register
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Support */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-900 uppercase tracking-wider mb-4">
|
||||
Support
|
||||
</h3>
|
||||
<ul className="space-y-2">
|
||||
<li>
|
||||
<Link to="/contact" className="text-gray-600 hover:text-primary-600 transition-colors">
|
||||
Contact Us
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 pt-8 border-t border-gray-200">
|
||||
<p className="text-center text-gray-500 text-sm">
|
||||
© {new Date().getFullYear()} spotlight.cam. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default PublicFooter;
|
||||
17
frontend/src/components/layout/PublicHeader.jsx
Normal file
17
frontend/src/components/layout/PublicHeader.jsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Video } from 'lucide-react';
|
||||
|
||||
const PublicHeader = () => {
|
||||
return (
|
||||
<header className="bg-white shadow-sm">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<Link to="/" className="flex items-center space-x-2 hover:opacity-80 transition-opacity">
|
||||
<Video className="w-8 h-8 text-primary-600" />
|
||||
<span className="text-xl font-bold text-gray-900">spotlight.cam</span>
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default PublicHeader;
|
||||
16
frontend/src/components/layout/PublicLayout.jsx
Normal file
16
frontend/src/components/layout/PublicLayout.jsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import PublicHeader from './PublicHeader';
|
||||
import PublicFooter from './PublicFooter';
|
||||
|
||||
const PublicLayout = ({ children }) => {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<PublicHeader />
|
||||
<main className="flex-grow">
|
||||
{children}
|
||||
</main>
|
||||
<PublicFooter />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PublicLayout;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import Layout from '../components/layout/Layout';
|
||||
import PublicLayout from '../components/layout/PublicLayout';
|
||||
|
||||
export default function AboutUsPage() {
|
||||
const [content, setContent] = useState('');
|
||||
@@ -29,32 +29,32 @@ export default function AboutUsPage() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<PublicLayout>
|
||||
<div className="bg-gray-50 flex items-center justify-center py-12">
|
||||
<div className="text-center">
|
||||
<div className="w-12 h-12 border-4 border-primary-600 border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
|
||||
<p className="text-gray-600">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
</PublicLayout>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<PublicLayout>
|
||||
<div className="bg-gray-50 flex items-center justify-center py-12">
|
||||
<div className="text-center">
|
||||
<p className="text-red-600">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
</PublicLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<PublicLayout>
|
||||
<div className="bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<article className="bg-white rounded-lg shadow-sm p-8 md:p-12 prose prose-lg max-w-none">
|
||||
<ReactMarkdown
|
||||
@@ -86,6 +86,6 @@ export default function AboutUsPage() {
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
</PublicLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { Send, Mail, User, MessageSquare } from 'lucide-react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { publicAPI } from '../services/api';
|
||||
import Layout from '../components/layout/Layout';
|
||||
import PublicLayout from '../components/layout/PublicLayout';
|
||||
|
||||
export default function ContactPage() {
|
||||
const { user } = useAuth();
|
||||
@@ -125,7 +125,7 @@ export default function ContactPage() {
|
||||
|
||||
if (success) {
|
||||
return (
|
||||
<Layout>
|
||||
<PublicLayout>
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center px-4">
|
||||
<div className="max-w-md w-full bg-white rounded-lg shadow-sm p-8 text-center">
|
||||
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
@@ -138,12 +138,12 @@ export default function ContactPage() {
|
||||
<p className="text-sm text-gray-500">Redirecting to homepage...</p>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
</PublicLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<PublicLayout>
|
||||
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
{/* Header */}
|
||||
@@ -312,6 +312,6 @@ export default function ContactPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
</PublicLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { authAPI } from '../services/api';
|
||||
import { Video, Mail, ArrowLeft, CheckCircle, Loader2 } from 'lucide-react';
|
||||
import PublicLayout from '../components/layout/PublicLayout';
|
||||
|
||||
const ForgotPasswordPage = () => {
|
||||
const [email, setEmail] = useState('');
|
||||
@@ -26,8 +27,9 @@ const ForgotPasswordPage = () => {
|
||||
|
||||
if (success) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4">
|
||||
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
|
||||
<PublicLayout>
|
||||
<div className="min-h-full bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4 py-12">
|
||||
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mb-4">
|
||||
<CheckCircle className="w-10 h-10 text-green-600" />
|
||||
@@ -50,13 +52,15 @@ const ForgotPasswordPage = () => {
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4">
|
||||
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
|
||||
<PublicLayout>
|
||||
<div className="min-h-full bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4 py-12">
|
||||
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
|
||||
<div className="flex flex-col items-center mb-6">
|
||||
<Video className="w-16 h-16 text-primary-600 mb-4" />
|
||||
<h1 className="text-3xl font-bold text-gray-900">Reset Password</h1>
|
||||
@@ -127,7 +131,8 @@ const ForgotPasswordPage = () => {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import Layout from '../components/layout/Layout';
|
||||
import PublicLayout from '../components/layout/PublicLayout';
|
||||
|
||||
export default function HowItWorksPage() {
|
||||
const [content, setContent] = useState('');
|
||||
@@ -29,32 +29,32 @@ export default function HowItWorksPage() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<PublicLayout>
|
||||
<div className="bg-gray-50 flex items-center justify-center py-12">
|
||||
<div className="text-center">
|
||||
<div className="w-12 h-12 border-4 border-primary-600 border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
|
||||
<p className="text-gray-600">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
</PublicLayout>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<PublicLayout>
|
||||
<div className="bg-gray-50 flex items-center justify-center py-12">
|
||||
<div className="text-center">
|
||||
<p className="text-red-600">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
</PublicLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<PublicLayout>
|
||||
<div className="bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<article className="bg-white rounded-lg shadow-sm p-8 md:p-12 prose prose-lg max-w-none">
|
||||
<ReactMarkdown
|
||||
@@ -86,6 +86,6 @@ export default function HowItWorksPage() {
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
</PublicLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Video, Mail, Lock } from 'lucide-react';
|
||||
import FormInput from '../components/common/FormInput';
|
||||
import LoadingButton from '../components/common/LoadingButton';
|
||||
import Alert from '../components/common/Alert';
|
||||
import PublicLayout from '../components/layout/PublicLayout';
|
||||
|
||||
const LoginPage = () => {
|
||||
const [email, setEmail] = useState('');
|
||||
@@ -30,13 +31,14 @@ const LoginPage = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4">
|
||||
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
|
||||
<div className="flex flex-col items-center mb-8">
|
||||
<Video className="w-16 h-16 text-primary-600 mb-4" />
|
||||
<h1 className="text-3xl font-bold text-gray-900">spotlight.cam</h1>
|
||||
<p className="text-gray-600 mt-2">Sign in to your account</p>
|
||||
</div>
|
||||
<PublicLayout>
|
||||
<div className="min-h-full bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4 py-12">
|
||||
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
|
||||
<div className="flex flex-col items-center mb-8">
|
||||
<Video className="w-16 h-16 text-primary-600 mb-4" />
|
||||
<h1 className="text-3xl font-bold text-gray-900">spotlight.cam</h1>
|
||||
<p className="text-gray-600 mt-2">Sign in to your account</p>
|
||||
</div>
|
||||
|
||||
<Alert type="error" message={error} />
|
||||
|
||||
@@ -94,7 +96,8 @@ const LoginPage = () => {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { Home, ArrowLeft } from 'lucide-react';
|
||||
import { publicAPI } from '../services/api';
|
||||
import PublicLayout from '../components/layout/PublicLayout';
|
||||
|
||||
export default function NotFoundPage() {
|
||||
const location = useLocation();
|
||||
@@ -21,18 +22,8 @@ export default function NotFoundPage() {
|
||||
}, [location.pathname, location.search]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Simple header */}
|
||||
<header className="bg-white shadow-sm">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<Link to="/" className="flex items-center gap-2 text-primary-600 hover:text-primary-700">
|
||||
<Home size={24} />
|
||||
<span className="text-xl font-bold">spotlight.cam</span>
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="min-h-screen flex items-center justify-center px-4 -mt-16">
|
||||
<PublicLayout>
|
||||
<div className="bg-gray-50 flex items-center justify-center px-4 py-12">
|
||||
<div className="max-w-md w-full text-center">
|
||||
{/* 404 Icon */}
|
||||
<div className="mb-8">
|
||||
@@ -80,6 +71,6 @@ export default function NotFoundPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import PasswordStrengthIndicator from '../components/common/PasswordStrengthIndi
|
||||
import FormInput from '../components/common/FormInput';
|
||||
import LoadingButton from '../components/common/LoadingButton';
|
||||
import Alert from '../components/common/Alert';
|
||||
import PublicLayout from '../components/layout/PublicLayout';
|
||||
|
||||
const RegisterPage = () => {
|
||||
// Step management
|
||||
@@ -218,8 +219,9 @@ const RegisterPage = () => {
|
||||
// Step 1: WSDC ID Check
|
||||
if (step === 1) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4">
|
||||
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
|
||||
<PublicLayout>
|
||||
<div className="min-h-full bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4 py-12">
|
||||
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
|
||||
<div className="flex flex-col items-center mb-8">
|
||||
<Video className="w-16 h-16 text-primary-600 mb-4" />
|
||||
<h1 className="text-3xl font-bold text-gray-900">spotlight.cam</h1>
|
||||
@@ -354,14 +356,16 @@ const RegisterPage = () => {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
);
|
||||
}
|
||||
|
||||
// Step 2: Registration Form
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4 py-8">
|
||||
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
|
||||
<PublicLayout>
|
||||
<div className="min-h-full bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4 py-12">
|
||||
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
|
||||
<div className="flex flex-col items-center mb-6">
|
||||
<Video className="w-12 h-12 text-primary-600 mb-3" />
|
||||
<h1 className="text-2xl font-bold text-gray-900">Complete your registration</h1>
|
||||
@@ -478,7 +482,8 @@ const RegisterPage = () => {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useNavigate, useSearchParams, Link } from 'react-router-dom';
|
||||
import { authAPI } from '../services/api';
|
||||
import { Video, Lock, CheckCircle, XCircle, Loader2 } from 'lucide-react';
|
||||
import PasswordStrengthIndicator from '../components/common/PasswordStrengthIndicator';
|
||||
import PublicLayout from '../components/layout/PublicLayout';
|
||||
|
||||
const ResetPasswordPage = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
@@ -50,146 +51,152 @@ const ResetPasswordPage = () => {
|
||||
// Success state
|
||||
if (success) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4">
|
||||
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mb-4">
|
||||
<CheckCircle className="w-10 h-10 text-green-600" />
|
||||
<PublicLayout>
|
||||
<div className="min-h-full bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4 py-12">
|
||||
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mb-4">
|
||||
<CheckCircle className="w-10 h-10 text-green-600" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-2">
|
||||
Password Reset Successfully! 🎉
|
||||
</h1>
|
||||
<p className="text-gray-600 mb-6">
|
||||
Your password has been updated. You can now log in with your new password.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => navigate('/login')}
|
||||
className="w-full py-2 px-4 border border-transparent rounded-md shadow-sm text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 font-medium"
|
||||
>
|
||||
Go to Login
|
||||
</button>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-2">
|
||||
Password Reset Successfully! 🎉
|
||||
</h1>
|
||||
<p className="text-gray-600 mb-6">
|
||||
Your password has been updated. You can now log in with your new password.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => navigate('/login')}
|
||||
className="w-full py-2 px-4 border border-transparent rounded-md shadow-sm text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 font-medium"
|
||||
>
|
||||
Go to Login
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
);
|
||||
}
|
||||
|
||||
// Invalid token state
|
||||
if (!token) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4">
|
||||
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mb-4">
|
||||
<XCircle className="w-10 h-10 text-red-600" />
|
||||
<PublicLayout>
|
||||
<div className="min-h-full bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4 py-12">
|
||||
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mb-4">
|
||||
<XCircle className="w-10 h-10 text-red-600" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-2">
|
||||
Invalid Reset Link
|
||||
</h1>
|
||||
<p className="text-gray-600 mb-6">
|
||||
This password reset link is invalid or has expired. Please request a new one.
|
||||
</p>
|
||||
<Link
|
||||
to="/forgot-password"
|
||||
className="w-full py-2 px-4 border border-transparent rounded-md shadow-sm text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 font-medium text-center"
|
||||
>
|
||||
Request New Link
|
||||
</Link>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-2">
|
||||
Invalid Reset Link
|
||||
</h1>
|
||||
<p className="text-gray-600 mb-6">
|
||||
This password reset link is invalid or has expired. Please request a new one.
|
||||
</p>
|
||||
<Link
|
||||
to="/forgot-password"
|
||||
className="w-full py-2 px-4 border border-transparent rounded-md shadow-sm text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 font-medium text-center"
|
||||
>
|
||||
Request New Link
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
);
|
||||
}
|
||||
|
||||
// Reset password form
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4">
|
||||
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
|
||||
<div className="flex flex-col items-center mb-6">
|
||||
<Video className="w-16 h-16 text-primary-600 mb-4" />
|
||||
<h1 className="text-3xl font-bold text-gray-900">Set New Password</h1>
|
||||
<p className="text-gray-600 mt-2 text-center">
|
||||
Enter your new password below
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-md flex items-start gap-2">
|
||||
<XCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-sm text-red-600">{error}</p>
|
||||
<PublicLayout>
|
||||
<div className="min-h-full bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4 py-12">
|
||||
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
|
||||
<div className="flex flex-col items-center mb-6">
|
||||
<Video className="w-16 h-16 text-primary-600 mb-4" />
|
||||
<h1 className="text-3xl font-bold text-gray-900">Set New Password</h1>
|
||||
<p className="text-gray-600 mt-2 text-center">
|
||||
Enter your new password below
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* New Password */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
New Password
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Lock className="h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
className="pl-10 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
disabled={loading}
|
||||
/>
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-md flex items-start gap-2">
|
||||
<XCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-sm text-red-600">{error}</p>
|
||||
</div>
|
||||
<PasswordStrengthIndicator password={newPassword} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Confirm Password */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Confirm New Password
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Lock className="h-5 w-5 text-gray-400" />
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* New Password */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
New Password
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Lock className="h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
className="pl-10 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
className="pl-10 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
disabled={loading}
|
||||
/>
|
||||
<PasswordStrengthIndicator password={newPassword} />
|
||||
</div>
|
||||
{confirmPassword && newPassword !== confirmPassword && (
|
||||
<p className="mt-1 text-sm text-red-600">Passwords do not match</p>
|
||||
)}
|
||||
|
||||
{/* Confirm Password */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Confirm New Password
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Lock className="h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
className="pl-10 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
{confirmPassword && newPassword !== confirmPassword && (
|
||||
<p className="mt-1 text-sm text-red-600">Passwords do not match</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || newPassword !== confirmPassword}
|
||||
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="w-5 h-5 animate-spin mr-2" />
|
||||
Resetting...
|
||||
</>
|
||||
) : (
|
||||
'Reset Password'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 text-center">
|
||||
<Link to="/login" className="text-sm font-medium text-primary-600 hover:text-primary-500">
|
||||
Back to Login
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || newPassword !== confirmPassword}
|
||||
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="w-5 h-5 animate-spin mr-2" />
|
||||
Resetting...
|
||||
</>
|
||||
) : (
|
||||
'Reset Password'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 text-center">
|
||||
<Link to="/login" className="text-sm font-medium text-primary-600 hover:text-primary-500">
|
||||
Back to Login
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useNavigate, useSearchParams, Link } from 'react-router-dom';
|
||||
import { authAPI } from '../services/api';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { Video, Mail, CheckCircle, XCircle, Loader2, ArrowRight } from 'lucide-react';
|
||||
import PublicLayout from '../components/layout/PublicLayout';
|
||||
|
||||
const VerifyEmailPage = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
@@ -101,147 +102,153 @@ const VerifyEmailPage = () => {
|
||||
// Success state
|
||||
if (success) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4">
|
||||
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mb-4">
|
||||
<CheckCircle className="w-10 h-10 text-green-600" />
|
||||
<PublicLayout>
|
||||
<div className="min-h-full bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4 py-12">
|
||||
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mb-4">
|
||||
<CheckCircle className="w-10 h-10 text-green-600" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-2">
|
||||
Email Verified! 🎉
|
||||
</h1>
|
||||
<p className="text-gray-600 mb-6">
|
||||
Your email has been successfully verified. You can now access all features of spotlight.cam!
|
||||
</p>
|
||||
<button
|
||||
onClick={() => navigate('/events')}
|
||||
className="w-full flex items-center justify-center gap-2 py-2 px-4 border border-transparent rounded-md shadow-sm text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 font-medium"
|
||||
>
|
||||
Go to Events
|
||||
<ArrowRight className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-2">
|
||||
Email Verified! 🎉
|
||||
</h1>
|
||||
<p className="text-gray-600 mb-6">
|
||||
Your email has been successfully verified. You can now access all features of spotlight.cam!
|
||||
</p>
|
||||
<button
|
||||
onClick={() => navigate('/events')}
|
||||
className="w-full flex items-center justify-center gap-2 py-2 px-4 border border-transparent rounded-md shadow-sm text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 font-medium"
|
||||
>
|
||||
Go to Events
|
||||
<ArrowRight className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
);
|
||||
}
|
||||
|
||||
// Loading state (for token verification)
|
||||
if (loading && verificationMode === 'token') {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4">
|
||||
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<Loader2 className="w-16 h-16 text-primary-600 mb-4 animate-spin" />
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-2">
|
||||
Verifying your email...
|
||||
</h1>
|
||||
<p className="text-gray-600">
|
||||
Please wait while we verify your email address.
|
||||
</p>
|
||||
<PublicLayout>
|
||||
<div className="min-h-full bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4 py-12">
|
||||
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<Loader2 className="w-16 h-16 text-primary-600 mb-4 animate-spin" />
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-2">
|
||||
Verifying your email...
|
||||
</h1>
|
||||
<p className="text-gray-600">
|
||||
Please wait while we verify your email address.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
);
|
||||
}
|
||||
|
||||
// Code verification form
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4">
|
||||
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
|
||||
<div className="flex flex-col items-center mb-6">
|
||||
<Video className="w-16 h-16 text-primary-600 mb-4" />
|
||||
<h1 className="text-3xl font-bold text-gray-900">Verify Your Email</h1>
|
||||
<p className="text-gray-600 mt-2 text-center">
|
||||
Enter the 6-digit code we sent to your email address
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-md flex items-start gap-2">
|
||||
<XCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-sm text-red-600">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleCodeVerification} className="space-y-4">
|
||||
{/* Email */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Email Address
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Mail className="h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="pl-10 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500"
|
||||
placeholder="your@email.com"
|
||||
required
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Verification Code */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Verification Code
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
|
||||
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 text-center text-2xl font-mono tracking-widest"
|
||||
placeholder="000000"
|
||||
maxLength="6"
|
||||
required
|
||||
disabled={loading}
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500 text-center">
|
||||
Enter the 6-digit code from your email
|
||||
<PublicLayout>
|
||||
<div className="min-h-full bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4 py-12">
|
||||
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
|
||||
<div className="flex flex-col items-center mb-6">
|
||||
<Video className="w-16 h-16 text-primary-600 mb-4" />
|
||||
<h1 className="text-3xl font-bold text-gray-900">Verify Your Email</h1>
|
||||
<p className="text-gray-600 mt-2 text-center">
|
||||
Enter the 6-digit code we sent to your email address
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="w-5 h-5 animate-spin mr-2" />
|
||||
Verifying...
|
||||
</>
|
||||
) : (
|
||||
'Verify Email'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-md flex items-start gap-2">
|
||||
<XCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-sm text-red-600">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleCodeVerification} className="space-y-4">
|
||||
{/* Email */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Email Address
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Mail className="h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="pl-10 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500"
|
||||
placeholder="your@email.com"
|
||||
required
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Verification Code */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Verification Code
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
|
||||
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 text-center text-2xl font-mono tracking-widest"
|
||||
placeholder="000000"
|
||||
maxLength="6"
|
||||
required
|
||||
disabled={loading}
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500 text-center">
|
||||
Enter the 6-digit code from your email
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 text-center">
|
||||
<p className="text-sm text-gray-600">
|
||||
Didn't receive the code?{' '}
|
||||
<button
|
||||
onClick={handleResendVerification}
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="font-medium text-primary-600 hover:text-primary-500 disabled:opacity-50"
|
||||
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50"
|
||||
>
|
||||
Resend
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="w-5 h-5 animate-spin mr-2" />
|
||||
Verifying...
|
||||
</>
|
||||
) : (
|
||||
'Verify Email'
|
||||
)}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="mt-4 text-center">
|
||||
<Link to="/events" className="text-sm text-gray-600 hover:text-gray-900">
|
||||
Skip for now →
|
||||
</Link>
|
||||
<div className="mt-6 text-center">
|
||||
<p className="text-sm text-gray-600">
|
||||
Didn't receive the code?{' '}
|
||||
<button
|
||||
onClick={handleResendVerification}
|
||||
disabled={loading}
|
||||
className="font-medium text-primary-600 hover:text-primary-500 disabled:opacity-50"
|
||||
>
|
||||
Resend
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-center">
|
||||
<Link to="/events" className="text-sm text-gray-600 hover:text-gray-900">
|
||||
Skip for now →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PublicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user