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,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