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:
Radosław Gierwiało
2025-12-05 21:59:56 +01:00
parent c47d182b98
commit 3ae9fd149b
13 changed files with 412 additions and 307 deletions

View File

@@ -1,26 +1,9 @@
# About Us Hi, Im 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, Ill probably be somewhere near the dance floor, probably pressing “record” on someones spotlight.
- **Duis aute irure** - Dolor in reprehenderit in voluptate velit esse cillum dolore 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 shouldnt 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 😄
- **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).*

View 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">
&copy; {new Date().getFullYear()} spotlight.cam. All rights reserved.
</p>
</div>
</div>
</footer>
);
};
export default PublicFooter;

View 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;

View 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;

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import Layout from '../components/layout/Layout'; import PublicLayout from '../components/layout/PublicLayout';
export default function AboutUsPage() { export default function AboutUsPage() {
const [content, setContent] = useState(''); const [content, setContent] = useState('');
@@ -29,32 +29,32 @@ export default function AboutUsPage() {
if (loading) { if (loading) {
return ( return (
<Layout> <PublicLayout>
<div className="min-h-screen bg-gray-50 flex items-center justify-center"> <div className="bg-gray-50 flex items-center justify-center py-12">
<div className="text-center"> <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> <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> <p className="text-gray-600">Loading...</p>
</div> </div>
</div> </div>
</Layout> </PublicLayout>
); );
} }
if (error) { if (error) {
return ( return (
<Layout> <PublicLayout>
<div className="min-h-screen bg-gray-50 flex items-center justify-center"> <div className="bg-gray-50 flex items-center justify-center py-12">
<div className="text-center"> <div className="text-center">
<p className="text-red-600">{error}</p> <p className="text-red-600">{error}</p>
</div> </div>
</div> </div>
</Layout> </PublicLayout>
); );
} }
return ( return (
<Layout> <PublicLayout>
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8"> <div className="bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-4xl mx-auto"> <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"> <article className="bg-white rounded-lg shadow-sm p-8 md:p-12 prose prose-lg max-w-none">
<ReactMarkdown <ReactMarkdown
@@ -86,6 +86,6 @@ export default function AboutUsPage() {
</article> </article>
</div> </div>
</div> </div>
</Layout> </PublicLayout>
); );
} }

View File

@@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
import { Send, Mail, User, MessageSquare } from 'lucide-react'; import { Send, Mail, User, MessageSquare } from 'lucide-react';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import { publicAPI } from '../services/api'; import { publicAPI } from '../services/api';
import Layout from '../components/layout/Layout'; import PublicLayout from '../components/layout/PublicLayout';
export default function ContactPage() { export default function ContactPage() {
const { user } = useAuth(); const { user } = useAuth();
@@ -125,7 +125,7 @@ export default function ContactPage() {
if (success) { if (success) {
return ( return (
<Layout> <PublicLayout>
<div className="min-h-screen bg-gray-50 flex items-center justify-center px-4"> <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="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"> <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> <p className="text-sm text-gray-500">Redirecting to homepage...</p>
</div> </div>
</div> </div>
</Layout> </PublicLayout>
); );
} }
return ( return (
<Layout> <PublicLayout>
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8"> <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"> <div className="max-w-2xl mx-auto">
{/* Header */} {/* Header */}
@@ -312,6 +312,6 @@ export default function ContactPage() {
</div> </div>
</div> </div>
</div> </div>
</Layout> </PublicLayout>
); );
} }

View File

@@ -2,6 +2,7 @@ import { useState } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { authAPI } from '../services/api'; import { authAPI } from '../services/api';
import { Video, Mail, ArrowLeft, CheckCircle, Loader2 } from 'lucide-react'; import { Video, Mail, ArrowLeft, CheckCircle, Loader2 } from 'lucide-react';
import PublicLayout from '../components/layout/PublicLayout';
const ForgotPasswordPage = () => { const ForgotPasswordPage = () => {
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
@@ -26,8 +27,9 @@ const ForgotPasswordPage = () => {
if (success) { if (success) {
return ( return (
<div className="min-h-screen bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4"> <PublicLayout>
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8"> <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="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"> <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" /> <CheckCircle className="w-10 h-10 text-green-600" />
@@ -50,13 +52,15 @@ const ForgotPasswordPage = () => {
</Link> </Link>
</div> </div>
</div> </div>
</div> </div>
</PublicLayout>
); );
} }
return ( return (
<div className="min-h-screen bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4"> <PublicLayout>
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8"> <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"> <div className="flex flex-col items-center mb-6">
<Video className="w-16 h-16 text-primary-600 mb-4" /> <Video className="w-16 h-16 text-primary-600 mb-4" />
<h1 className="text-3xl font-bold text-gray-900">Reset Password</h1> <h1 className="text-3xl font-bold text-gray-900">Reset Password</h1>
@@ -127,7 +131,8 @@ const ForgotPasswordPage = () => {
</p> </p>
</div> </div>
</div> </div>
</div> </div>
</PublicLayout>
); );
}; };

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import Layout from '../components/layout/Layout'; import PublicLayout from '../components/layout/PublicLayout';
export default function HowItWorksPage() { export default function HowItWorksPage() {
const [content, setContent] = useState(''); const [content, setContent] = useState('');
@@ -29,32 +29,32 @@ export default function HowItWorksPage() {
if (loading) { if (loading) {
return ( return (
<Layout> <PublicLayout>
<div className="min-h-screen bg-gray-50 flex items-center justify-center"> <div className="bg-gray-50 flex items-center justify-center py-12">
<div className="text-center"> <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> <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> <p className="text-gray-600">Loading...</p>
</div> </div>
</div> </div>
</Layout> </PublicLayout>
); );
} }
if (error) { if (error) {
return ( return (
<Layout> <PublicLayout>
<div className="min-h-screen bg-gray-50 flex items-center justify-center"> <div className="bg-gray-50 flex items-center justify-center py-12">
<div className="text-center"> <div className="text-center">
<p className="text-red-600">{error}</p> <p className="text-red-600">{error}</p>
</div> </div>
</div> </div>
</Layout> </PublicLayout>
); );
} }
return ( return (
<Layout> <PublicLayout>
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8"> <div className="bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-4xl mx-auto"> <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"> <article className="bg-white rounded-lg shadow-sm p-8 md:p-12 prose prose-lg max-w-none">
<ReactMarkdown <ReactMarkdown
@@ -86,6 +86,6 @@ export default function HowItWorksPage() {
</article> </article>
</div> </div>
</div> </div>
</Layout> </PublicLayout>
); );
} }

View File

@@ -5,6 +5,7 @@ import { Video, Mail, Lock } from 'lucide-react';
import FormInput from '../components/common/FormInput'; import FormInput from '../components/common/FormInput';
import LoadingButton from '../components/common/LoadingButton'; import LoadingButton from '../components/common/LoadingButton';
import Alert from '../components/common/Alert'; import Alert from '../components/common/Alert';
import PublicLayout from '../components/layout/PublicLayout';
const LoginPage = () => { const LoginPage = () => {
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
@@ -30,13 +31,14 @@ const LoginPage = () => {
}; };
return ( return (
<div className="min-h-screen bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4"> <PublicLayout>
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8"> <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="flex flex-col items-center mb-8"> <div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
<Video className="w-16 h-16 text-primary-600 mb-4" /> <div className="flex flex-col items-center mb-8">
<h1 className="text-3xl font-bold text-gray-900">spotlight.cam</h1> <Video className="w-16 h-16 text-primary-600 mb-4" />
<p className="text-gray-600 mt-2">Sign in to your account</p> <h1 className="text-3xl font-bold text-gray-900">spotlight.cam</h1>
</div> <p className="text-gray-600 mt-2">Sign in to your account</p>
</div>
<Alert type="error" message={error} /> <Alert type="error" message={error} />
@@ -94,7 +96,8 @@ const LoginPage = () => {
</p> </p>
</div> </div>
</div> </div>
</div> </div>
</PublicLayout>
); );
}; };

View File

@@ -2,6 +2,7 @@ import { useEffect } from 'react';
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import { Home, ArrowLeft } from 'lucide-react'; import { Home, ArrowLeft } from 'lucide-react';
import { publicAPI } from '../services/api'; import { publicAPI } from '../services/api';
import PublicLayout from '../components/layout/PublicLayout';
export default function NotFoundPage() { export default function NotFoundPage() {
const location = useLocation(); const location = useLocation();
@@ -21,18 +22,8 @@ export default function NotFoundPage() {
}, [location.pathname, location.search]); }, [location.pathname, location.search]);
return ( return (
<div className="min-h-screen bg-gray-50"> <PublicLayout>
{/* Simple header */} <div className="bg-gray-50 flex items-center justify-center px-4 py-12">
<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">
<div className="max-w-md w-full text-center"> <div className="max-w-md w-full text-center">
{/* 404 Icon */} {/* 404 Icon */}
<div className="mb-8"> <div className="mb-8">
@@ -80,6 +71,6 @@ export default function NotFoundPage() {
</div> </div>
</div> </div>
</div> </div>
</div> </PublicLayout>
); );
} }

View File

@@ -7,6 +7,7 @@ import PasswordStrengthIndicator from '../components/common/PasswordStrengthIndi
import FormInput from '../components/common/FormInput'; import FormInput from '../components/common/FormInput';
import LoadingButton from '../components/common/LoadingButton'; import LoadingButton from '../components/common/LoadingButton';
import Alert from '../components/common/Alert'; import Alert from '../components/common/Alert';
import PublicLayout from '../components/layout/PublicLayout';
const RegisterPage = () => { const RegisterPage = () => {
// Step management // Step management
@@ -218,8 +219,9 @@ const RegisterPage = () => {
// Step 1: WSDC ID Check // Step 1: WSDC ID Check
if (step === 1) { if (step === 1) {
return ( return (
<div className="min-h-screen bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4"> <PublicLayout>
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8"> <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"> <div className="flex flex-col items-center mb-8">
<Video className="w-16 h-16 text-primary-600 mb-4" /> <Video className="w-16 h-16 text-primary-600 mb-4" />
<h1 className="text-3xl font-bold text-gray-900">spotlight.cam</h1> <h1 className="text-3xl font-bold text-gray-900">spotlight.cam</h1>
@@ -354,14 +356,16 @@ const RegisterPage = () => {
</p> </p>
</div> </div>
</div> </div>
</div> </div>
</PublicLayout>
); );
} }
// Step 2: Registration Form // Step 2: Registration Form
return ( 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"> <PublicLayout>
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8"> <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"> <div className="flex flex-col items-center mb-6">
<Video className="w-12 h-12 text-primary-600 mb-3" /> <Video className="w-12 h-12 text-primary-600 mb-3" />
<h1 className="text-2xl font-bold text-gray-900">Complete your registration</h1> <h1 className="text-2xl font-bold text-gray-900">Complete your registration</h1>
@@ -478,7 +482,8 @@ const RegisterPage = () => {
</p> </p>
</div> </div>
</div> </div>
</div> </div>
</PublicLayout>
); );
}; };

View File

@@ -3,6 +3,7 @@ import { useNavigate, useSearchParams, Link } from 'react-router-dom';
import { authAPI } from '../services/api'; import { authAPI } from '../services/api';
import { Video, Lock, CheckCircle, XCircle, Loader2 } from 'lucide-react'; import { Video, Lock, CheckCircle, XCircle, Loader2 } from 'lucide-react';
import PasswordStrengthIndicator from '../components/common/PasswordStrengthIndicator'; import PasswordStrengthIndicator from '../components/common/PasswordStrengthIndicator';
import PublicLayout from '../components/layout/PublicLayout';
const ResetPasswordPage = () => { const ResetPasswordPage = () => {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
@@ -50,146 +51,152 @@ const ResetPasswordPage = () => {
// Success state // Success state
if (success) { if (success) {
return ( return (
<div className="min-h-screen bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4"> <PublicLayout>
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8"> <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="flex flex-col items-center text-center"> <div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mb-4"> <div className="flex flex-col items-center text-center">
<CheckCircle className="w-10 h-10 text-green-600" /> <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> </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> </div>
</div> </PublicLayout>
); );
} }
// Invalid token state // Invalid token state
if (!token) { if (!token) {
return ( return (
<div className="min-h-screen bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4"> <PublicLayout>
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8"> <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="flex flex-col items-center text-center"> <div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mb-4"> <div className="flex flex-col items-center text-center">
<XCircle className="w-10 h-10 text-red-600" /> <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> </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> </div>
</div> </PublicLayout>
); );
} }
// Reset password form // Reset password form
return ( return (
<div className="min-h-screen bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4"> <PublicLayout>
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8"> <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="flex flex-col items-center mb-6"> <div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
<Video className="w-16 h-16 text-primary-600 mb-4" /> <div className="flex flex-col items-center mb-6">
<h1 className="text-3xl font-bold text-gray-900">Set New Password</h1> <Video className="w-16 h-16 text-primary-600 mb-4" />
<p className="text-gray-600 mt-2 text-center"> <h1 className="text-3xl font-bold text-gray-900">Set New Password</h1>
Enter your new password below <p className="text-gray-600 mt-2 text-center">
</p> Enter your new password below
</div> </p>
{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> </div>
)}
<form onSubmit={handleSubmit} className="space-y-4"> {error && (
{/* New Password */} <div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-md flex items-start gap-2">
<div> <XCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
<label className="block text-sm font-medium text-gray-700 mb-2"> <p className="text-sm text-red-600">{error}</p>
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> </div>
<PasswordStrengthIndicator password={newPassword} /> )}
</div>
{/* Confirm Password */} <form onSubmit={handleSubmit} className="space-y-4">
<div> {/* New Password */}
<label className="block text-sm font-medium text-gray-700 mb-2"> <div>
Confirm New Password <label className="block text-sm font-medium text-gray-700 mb-2">
</label> New Password
<div className="relative"> </label>
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> <div className="relative">
<Lock className="h-5 w-5 text-gray-400" /> <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> </div>
<input <PasswordStrengthIndicator password={newPassword} />
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> </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> </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> </div>
</div> </PublicLayout>
); );
}; };

View File

@@ -3,6 +3,7 @@ import { useNavigate, useSearchParams, Link } from 'react-router-dom';
import { authAPI } from '../services/api'; import { authAPI } from '../services/api';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import { Video, Mail, CheckCircle, XCircle, Loader2, ArrowRight } from 'lucide-react'; import { Video, Mail, CheckCircle, XCircle, Loader2, ArrowRight } from 'lucide-react';
import PublicLayout from '../components/layout/PublicLayout';
const VerifyEmailPage = () => { const VerifyEmailPage = () => {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
@@ -101,147 +102,153 @@ const VerifyEmailPage = () => {
// Success state // Success state
if (success) { if (success) {
return ( return (
<div className="min-h-screen bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4"> <PublicLayout>
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8"> <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="flex flex-col items-center text-center"> <div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mb-4"> <div className="flex flex-col items-center text-center">
<CheckCircle className="w-10 h-10 text-green-600" /> <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> </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> </div>
</div> </PublicLayout>
); );
} }
// Loading state (for token verification) // Loading state (for token verification)
if (loading && verificationMode === 'token') { if (loading && verificationMode === 'token') {
return ( return (
<div className="min-h-screen bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4"> <PublicLayout>
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8"> <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="flex flex-col items-center text-center"> <div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
<Loader2 className="w-16 h-16 text-primary-600 mb-4 animate-spin" /> <div className="flex flex-col items-center text-center">
<h1 className="text-2xl font-bold text-gray-900 mb-2"> <Loader2 className="w-16 h-16 text-primary-600 mb-4 animate-spin" />
Verifying your email... <h1 className="text-2xl font-bold text-gray-900 mb-2">
</h1> Verifying your email...
<p className="text-gray-600"> </h1>
Please wait while we verify your email address. <p className="text-gray-600">
</p> Please wait while we verify your email address.
</p>
</div>
</div> </div>
</div> </div>
</div> </PublicLayout>
); );
} }
// Code verification form // Code verification form
return ( return (
<div className="min-h-screen bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center px-4"> <PublicLayout>
<div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8"> <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="flex flex-col items-center mb-6"> <div className="max-w-md w-full bg-white rounded-lg shadow-xl p-8">
<Video className="w-16 h-16 text-primary-600 mb-4" /> <div className="flex flex-col items-center mb-6">
<h1 className="text-3xl font-bold text-gray-900">Verify Your Email</h1> <Video className="w-16 h-16 text-primary-600 mb-4" />
<p className="text-gray-600 mt-2 text-center"> <h1 className="text-3xl font-bold text-gray-900">Verify Your Email</h1>
Enter the 6-digit code we sent to your email address <p className="text-gray-600 mt-2 text-center">
</p> Enter the 6-digit code we sent to your email address
</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
</p> </p>
</div> </div>
<button {error && (
type="submit" <div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-md flex items-start gap-2">
disabled={loading} <XCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
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" <p className="text-sm text-red-600">{error}</p>
> </div>
{loading ? ( )}
<>
<Loader2 className="w-5 h-5 animate-spin mr-2" /> <form onSubmit={handleCodeVerification} className="space-y-4">
Verifying... {/* Email */}
</> <div>
) : ( <label className="block text-sm font-medium text-gray-700 mb-2">
'Verify Email' Email Address
)} </label>
</button> <div className="relative">
</form> <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 <button
onClick={handleResendVerification} type="submit"
disabled={loading} 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> </button>
</p> </form>
</div>
<div className="mt-4 text-center"> <div className="mt-6 text-center">
<Link to="/events" className="text-sm text-gray-600 hover:text-gray-900"> <p className="text-sm text-gray-600">
Skip for now Didn't receive the code?{' '}
</Link> <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> </div>
</div> </PublicLayout>
); );
}; };