refactored Authentication

This commit is contained in:
2026-02-17 16:59:02 +00:00
parent d32d0f5c03
commit f80e81c18b
12 changed files with 19 additions and 976 deletions
@@ -1,10 +0,0 @@
import { AuthLayout } from './Layout';
import { Hero } from './components/Hero';
export function LandingPage() {
return (
<AuthLayout>
<Hero />
</AuthLayout>
);
}
@@ -1,56 +0,0 @@
import type { ReactNode } from 'react';
import { useLocation, useNavigate } from 'react-router';
import { useAuth } from 'hooks/useAuth';
import { PixelGrid } from '../../../../workspaces/components/PixelGrid';
import { DuckAvatar } from './components/DuckAvatar';
import { SignupModal } from './components/SignupModal';
import { LoginModal } from './components/LoginModal';
import { ForgotPasswordModal } from './components/ForgotPasswordModal';
import { VerifyModal } from './components/VerifyModal';
import { ResetPasswordModal } from './components/ResetPasswordModal';
type AuthLayoutProps = {
children?: ReactNode;
};
export function AuthLayout({ children }: AuthLayoutProps) {
const location = useLocation();
const navigate = useNavigate();
const { registrationOpen } = useAuth();
const signupOpen = registrationOpen && location.pathname === '/auth/register';
const loginOpen = location.pathname === '/auth/login';
const forgotPasswordOpen = location.pathname === '/auth/forgot-password';
const verifyOpen = location.pathname === '/auth/verify';
const resetPasswordOpen = location.pathname === '/auth/reset-password';
const closeModal = (open: boolean) => !open && navigate('/');
return (
<div className="relative overflow-hidden h-dvh outline-none fixed inset-0">
<PixelGrid />
<DuckAvatar showDebug={false} fullControlMode={false} currentSection={0} />
<section className="relative h-dvh snap-start overflow-hidden">
{/* Background layer */}
<div
className="absolute inset-0 z-0"
style={{
backgroundImage: 'var(--page-bg-image)',
backgroundSize: 'cover',
backgroundPosition: 'center center',
}}
/>
{/* Content layer - above duck and pixel grid */}
<div className="absolute inset-0 z-[520]">{children}</div>
</section>
<SignupModal open={signupOpen} onOpenChange={closeModal} />
<LoginModal open={loginOpen} onOpenChange={closeModal} />
<ForgotPasswordModal open={forgotPasswordOpen} onOpenChange={closeModal} />
<VerifyModal open={verifyOpen} onOpenChange={closeModal} />
<ResetPasswordModal open={resetPasswordOpen} onOpenChange={closeModal} />
</div>
);
}
@@ -1,123 +0,0 @@
import { useState } from 'react';
import { toast } from 'sonner';
import { DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
import { Link } from 'react-router';
import { PaperDialog } from '@/components/Dialogs';
import { useForm } from 'hooks/useForm';
import { useAuth } from 'hooks/useAuth';
type ForgotPasswordFormState = {
email?: string;
};
const validateForm = (state: Partial<ForgotPasswordFormState>) => {
return !!state.email;
};
type ForgotPasswordModalProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
};
export const ForgotPasswordModal = ({ open, onOpenChange }: ForgotPasswordModalProps) => {
const [isSubmitting, setIsSubmitting] = useState(false);
const [sent, setSent] = useState(false);
const { state, formRef, update, isValid } = useForm<ForgotPasswordFormState>({}, validateForm);
const { forgotPassword } = useAuth();
const handleSubmit = async (ev: React.FormEvent) => {
ev.preventDefault();
if (!isValid || isSubmitting) return;
setIsSubmitting(true);
try {
await forgotPassword({ email: state.email! });
setSent(true);
} catch (ex) {
const error = ex as { message?: string };
toast.error(error.message || 'Failed to send recovery email. Please try again.');
} finally {
setIsSubmitting(false);
}
};
const handleClose = (isOpen: boolean) => {
if (!isOpen) {
setSent(false);
if (formRef.current) {
update({ email: '' });
}
}
onOpenChange(isOpen);
};
const isDisabled = !isValid || isSubmitting;
return (
<PaperDialog
open={open}
onOpenChange={handleClose}
onOpenAutoFocus={(ev) => {
if (!sent) {
const firstInput = (ev.currentTarget as HTMLElement | null)?.querySelector('input');
firstInput?.focus();
}
}}
>
{sent ? (
<div className="text-center py-4">
<h2 className="text-2xl font-bold text-duck-dark">Email Sent</h2>
<p className="mt-4 text-duck-dark/70">Please check your email for further instructions.</p>
<Button
onClick={() => handleClose(false)}
className="mt-6 bg-duck-yellow hover:bg-duck-yellow/90 text-duck-teal font-bold px-8 py-3 transition-all duration-200 hover:scale-105 cursor-pointer"
>
Close
</Button>
</div>
) : (
<>
<DialogHeader>
<DialogTitle className="text-duck-dark text-2xl font-bold">Forgot Password</DialogTitle>
<DialogDescription className="text-duck-dark/60">
Enter your email and we'll send you a recovery link
</DialogDescription>
</DialogHeader>
<form ref={formRef} onSubmit={handleSubmit} className="grid gap-4">
<Label className="grid gap-2">
<span className="text-duck-dark/70">Email</span>
<Input
className="h-11 bg-white/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
type="email"
name="email"
placeholder="you@example.com"
autoComplete="email"
/>
</Label>
<DialogFooter className="pt-2">
<Button
type="submit"
disabled={isDisabled}
className="w-full h-11 bg-duck-yellow hover:bg-duck-yellow/90 text-duck-teal font-bold text-lg transition-all duration-200 hover:scale-105 cursor-pointer disabled:opacity-50 disabled:hover:scale-100"
>
{isSubmitting ? 'Sending...' : 'Send Recovery Email'}
</Button>
</DialogFooter>
<p className="text-center text-sm text-duck-dark/60">
Remember your password?{' '}
<Link to="/auth/login" className="text-duck-forest underline hover:text-duck-forest/80">
Sign In
</Link>
</p>
</form>
</>
)}
</PaperDialog>
);
};
@@ -1,95 +0,0 @@
import { useState } from 'react';
import { useNavigate } from 'react-router';
import { toast } from 'sonner';
import { useAuth } from 'hooks/useAuth';
import { Button } from '@/components/ui/button';
import { OfficerSvgLogo } from '../Logos';
export function Hero() {
const navigate = useNavigate();
const { registrationOpen, signup } = useAuth();
const [email, setEmail] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [done, setDone] = useState(false);
const handleBootstrap = async () => {
const trimmed = email.trim();
if (!trimmed || isSubmitting) return;
setIsSubmitting(true);
try {
await signup({ email: trimmed });
setDone(true);
} catch (ex) {
const error = ex as { message?: string };
toast.error(error.message || 'Bootstrap failed. Please try again.');
} finally {
setIsSubmitting(false);
}
};
return (
<>
<header className="pt-28 md:pt-20 lg:pt-24 xl:pt-0 z-20 flex justify-center">
<div className="hidden md:block">
<OfficerSvgLogo size="2xl" />
</div>
<div className="block md:hidden">
<OfficerSvgLogo size="lg" />
</div>
</header>
{registrationOpen ? (
<div className="absolute inset-x-0 bottom-0 z-20 flex justify-center pb-8 md:pb-12">
<div className="bg-white/90 border-2 border-duck-dark/20 rounded-2xl p-8 max-w-md w-full mx-4 shadow-xl">
{done ? (
<div className="text-center">
<h2 className="text-2xl font-bold text-duck-dark mb-2">Check your email</h2>
<p className="text-sm text-duck-dark/60">
A verification link has been sent to <strong>{email}</strong>. Click it to activate your account.
</p>
</div>
) : (
<>
<h2 className="text-2xl font-bold text-duck-dark mb-2">Welcome, Admin</h2>
<p className="text-sm text-duck-dark/60 mb-6">Enter your email to create your administrator account.</p>
<div className="flex gap-2">
<input
type="email"
value={email}
onChange={(ev) => setEmail(ev.target.value)}
onKeyDown={(ev) => {
if (ev.key === 'Enter') {
ev.preventDefault();
handleBootstrap();
}
}}
placeholder="admin@example.com"
className="flex-1 px-4 py-2.5 rounded-lg border-2 border-duck-dark/20 text-sm text-duck-dark placeholder:text-duck-dark/30 focus:outline-none focus:border-duck-teal/50"
/>
<Button
onClick={handleBootstrap}
disabled={!email.trim() || isSubmitting}
className="bg-duck-yellow hover:bg-duck-yellow/90 text-duck-teal font-bold px-6 py-2.5 rounded-lg transition-all duration-200 hover:scale-105 cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"
>
{isSubmitting ? 'Sending...' : 'Bootstrap'}
</Button>
</div>
</>
)}
</div>
</div>
) : (
<div className="absolute top-4 right-4 md:top-6 md:right-6 z-20">
<Button
size="default"
onClick={() => navigate('/auth/login')}
className="bg-duck-teal hover:bg-duck-teal/90 text-duck-yellow rounded-lg font-bold transition-all duration-200 hover:scale-105 cursor-pointer px-12 py-5 text-lg md:text-xl lg:text-2xl"
>
Login
</Button>
</div>
)}
</>
);
}
@@ -1 +0,0 @@
export * from './Hero';
@@ -1,124 +0,0 @@
import { useState } from 'react';
import { toast } from 'sonner';
import { DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
import { Link } from 'react-router';
import { PaperDialog } from '@/components/Dialogs';
import { useForm } from 'hooks/useForm';
import { useAuth } from 'hooks/useAuth';
type LoginFormState = {
email?: string;
password?: string;
};
const validateForm = (state: Partial<LoginFormState>) => {
const { email, password } = state;
if (!email || !password) return false;
return true;
};
type LoginModalProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
};
export const LoginModal = ({ open, onOpenChange }: LoginModalProps) => {
const [isSubmitting, setIsSubmitting] = useState(false);
const { state, formRef, update, isValid } = useForm<LoginFormState>({}, validateForm);
const { signin } = useAuth();
const handleSubmit = async (ev: React.FormEvent) => {
ev.preventDefault();
if (!isValid || isSubmitting) return;
setIsSubmitting(true);
try {
await signin({ email: state.email!, password: state.password! });
window.location.href = '/';
} catch (ex) {
const error = ex as { message?: string };
toast.error(error.message || 'Login failed. Please try again.');
update({ ...state, password: '' });
} finally {
setIsSubmitting(false);
}
};
const handleClose = (isOpen: boolean) => {
if (!isOpen) {
if (formRef.current) {
update({ email: '', password: '' });
}
}
onOpenChange(isOpen);
};
const isDisabled = !isValid || isSubmitting;
return (
<PaperDialog
open={open}
onOpenChange={handleClose}
onOpenAutoFocus={(ev) => {
const firstInput = (ev.currentTarget as HTMLElement | null)?.querySelector('input');
firstInput?.focus();
}}
>
<DialogHeader>
<DialogTitle className="text-duck-dark text-2xl font-bold">Welcome Back</DialogTitle>
<DialogDescription className="text-duck-dark/60">Sign in to your account</DialogDescription>
</DialogHeader>
<form ref={formRef} onSubmit={handleSubmit} className="grid gap-4">
<Label className="grid gap-2">
<span className="text-duck-dark/70">Email</span>
<Input
className="h-11 bg-white/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
type="email"
name="email"
placeholder="you@example.com"
autoComplete="email"
/>
</Label>
<Label className="grid gap-2">
<span className="text-duck-dark/70">Password</span>
<Input
className="h-11 bg-white/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
type="password"
name="password"
placeholder="Your password"
autoComplete="current-password"
/>
</Label>
<DialogFooter className="pt-2">
<Button
type="submit"
disabled={isDisabled}
className="w-full h-11 bg-duck-yellow hover:bg-duck-yellow/90 text-duck-teal font-bold text-lg transition-all duration-200 hover:scale-105 cursor-pointer disabled:opacity-50 disabled:hover:scale-100"
>
{isSubmitting ? 'Signing in...' : 'Sign In'}
</Button>
</DialogFooter>
<div className="text-center text-sm text-duck-dark/60 grid gap-1">
<p>
Don't have an account?{' '}
<Link to="/auth/register" className="text-duck-forest underline hover:text-duck-forest/80">
Register
</Link>
</p>
<p>
<Link to="/auth/forgot-password" className="text-duck-forest underline hover:text-duck-forest/80">
Forgot password?
</Link>
</p>
</div>
</form>
</PaperDialog>
);
};
@@ -1,138 +0,0 @@
import { useState } from 'react';
import { toast } from 'sonner';
import { DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
import { Link } from 'react-router';
import { PaperDialog } from '@/components/Dialogs';
import { useForm } from 'hooks/useForm';
import { useAuth } from 'hooks/useAuth';
type ResetPasswordFormState = {
password?: string;
confirmPassword?: string;
};
const validateForm = (state: Partial<ResetPasswordFormState>) => {
const { password, confirmPassword } = state;
if (!password || !confirmPassword) return false;
if (password !== confirmPassword) return false;
return true;
};
type ResetPasswordModalProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
};
export const ResetPasswordModal = ({ open, onOpenChange }: ResetPasswordModalProps) => {
const [isSubmitting, setIsSubmitting] = useState(false);
const [reset, setReset] = useState(false);
const { state, formRef, update, isValid } = useForm<ResetPasswordFormState>({}, validateForm);
const { resetPassword } = useAuth();
const handleSubmit = async (ev: React.FormEvent) => {
ev.preventDefault();
if (!isValid || isSubmitting) return;
setIsSubmitting(true);
try {
const verificationCode = new URL(window.location.href).searchParams.get('verificationCode');
if (!verificationCode) throw new Error('Invalid verification code');
await resetPassword({ password: state.password!, verificationCode });
setReset(true);
} catch (ex) {
const error = ex as { message?: string };
toast.error(error.message || 'Password reset failed. Please try again.');
update({ password: '', confirmPassword: '' });
} finally {
setIsSubmitting(false);
}
};
const handleClose = (isOpen: boolean) => {
if (!isOpen) {
setReset(false);
if (formRef.current) {
update({ password: '', confirmPassword: '' });
}
}
onOpenChange(isOpen);
};
const isDisabled = !isValid || isSubmitting;
return (
<PaperDialog
open={open}
onOpenChange={handleClose}
onOpenAutoFocus={(ev) => {
if (!reset) {
const firstInput = (ev.currentTarget as HTMLElement | null)?.querySelector('input');
firstInput?.focus();
}
}}
>
{reset ? (
<div className="text-center py-4">
<h2 className="text-2xl font-bold text-duck-dark">Password Reset</h2>
<p className="mt-4 text-duck-dark/70">Your password has been reset. You can now sign in.</p>
<Button
onClick={() => (window.location.href = '/auth/login')}
className="mt-6 bg-duck-yellow hover:bg-duck-yellow/90 text-duck-teal font-bold px-8 py-3 transition-all duration-200 hover:scale-105 cursor-pointer"
>
Sign In
</Button>
</div>
) : (
<>
<DialogHeader>
<DialogTitle className="text-duck-dark text-2xl font-bold">Reset Password</DialogTitle>
<DialogDescription className="text-duck-dark/60">Enter your new password</DialogDescription>
</DialogHeader>
<form ref={formRef} onSubmit={handleSubmit} className="grid gap-4">
<Label className="grid gap-2">
<span className="text-duck-dark/70">New Password</span>
<Input
className="h-11 bg-white/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
type="password"
name="password"
placeholder="Min 12 chars, mixed case, number, symbol"
autoComplete="new-password"
/>
</Label>
<Label className="grid gap-2">
<span className="text-duck-dark/70">Confirm Password</span>
<Input
className="h-11 bg-white/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
type="password"
name="confirmPassword"
placeholder="Repeat your password"
autoComplete="new-password"
/>
</Label>
<DialogFooter className="pt-2">
<Button
type="submit"
disabled={isDisabled}
className="w-full h-11 bg-duck-yellow hover:bg-duck-yellow/90 text-duck-teal font-bold text-lg transition-all duration-200 hover:scale-105 cursor-pointer disabled:opacity-50 disabled:hover:scale-100"
>
{isSubmitting ? 'Resetting...' : 'Reset Password'}
</Button>
</DialogFooter>
<p className="text-center text-sm text-duck-dark/60">
<Link to="/auth/login" className="text-duck-forest underline hover:text-duck-forest/80">
Back to Sign In
</Link>
</p>
</form>
</>
)}
</PaperDialog>
);
};
@@ -1,164 +0,0 @@
import { useState } from 'react';
import { toast } from 'sonner';
import { useLocation, Link } from 'react-router';
import { DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
import { PaperDialog } from '@/components/Dialogs';
import { useForm } from 'hooks/useForm';
import { useAuth } from 'hooks/useAuth';
type SignupFormState = {
name?: string;
email?: string;
password?: string;
confirmPassword?: string;
};
const validateForm = (state: Partial<SignupFormState>) => {
const { name, email, password, confirmPassword } = state;
if (!name || !email || !password || !confirmPassword) return false;
if (password !== confirmPassword) return false;
return true;
};
type SignupModalProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
};
export const SignupModal = ({ open, onOpenChange }: SignupModalProps) => {
const location = useLocation();
const bootstrapEmail = (location.state as { email?: string } | null)?.email ?? '';
const [isSubmitting, setIsSubmitting] = useState(false);
const [registered, setRegistered] = useState(false);
const { state, formRef, update, isValid } = useForm<SignupFormState>({ email: bootstrapEmail }, validateForm);
const { signup } = useAuth();
const handleSubmit = async (ev: React.FormEvent) => {
ev.preventDefault();
if (!isValid || isSubmitting) return;
setIsSubmitting(true);
try {
await signup({ name: state.name, email: state.email, password: state.password });
setRegistered(true);
} catch (ex) {
const error = ex as { message?: string };
toast.error(error.message || 'Signup failed. Please try again.');
update({ ...state, password: '', confirmPassword: '' });
} finally {
setIsSubmitting(false);
}
};
const handleClose = (isOpen: boolean) => {
if (!isOpen) {
setRegistered(false);
if (formRef.current) {
update({ name: '', email: '', password: '', confirmPassword: '' });
}
}
onOpenChange(isOpen);
};
const isDisabled = !isValid || isSubmitting;
return (
<PaperDialog
open={open}
onOpenChange={handleClose}
onOpenAutoFocus={(ev) => {
if (!registered) {
const firstInput = (ev.currentTarget as HTMLElement).querySelector('input');
firstInput?.focus();
}
}}
>
{registered ? (
<div className="text-center py-4">
<h2 className="text-2xl font-bold text-duck-dark">Registration Successful</h2>
<p className="mt-4 text-duck-dark/70">Please check your email to verify your account.</p>
<Button
onClick={() => handleClose(false)}
className="mt-6 bg-duck-yellow hover:bg-duck-yellow/90 text-duck-teal font-bold px-8 py-3 transition-all duration-200 hover:scale-105 cursor-pointer"
>
Close
</Button>
</div>
) : (
<>
<DialogHeader>
<DialogTitle className="text-duck-dark text-2xl font-bold">Create Account</DialogTitle>
<DialogDescription className="text-duck-dark/60">Create a new account to get started</DialogDescription>
</DialogHeader>
<form ref={formRef} onSubmit={handleSubmit} className="grid gap-4">
<Label className="grid gap-2">
<span className="text-duck-dark/70">Name</span>
<Input
className="h-11 bg-white/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
type="text"
name="name"
placeholder="Your name"
autoComplete="name"
/>
</Label>
<Label className="grid gap-2">
<span className="text-duck-dark/70">Email</span>
<Input
className="h-11 bg-white/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
type="email"
name="email"
defaultValue={bootstrapEmail}
placeholder="you@example.com"
autoComplete="email"
/>
</Label>
<Label className="grid gap-2">
<span className="text-duck-dark/70">Password</span>
<Input
className="h-11 bg-white/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
type="password"
name="password"
placeholder="Min 12 chars, mixed case, number, symbol"
autoComplete="new-password"
/>
</Label>
<Label className="grid gap-2">
<span className="text-duck-dark/70">Confirm Password</span>
<Input
className="h-11 bg-white/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
type="password"
name="confirmPassword"
placeholder="Repeat your password"
autoComplete="new-password"
/>
</Label>
<DialogFooter className="pt-2">
<Button
type="submit"
disabled={isDisabled}
className="w-full h-11 bg-duck-yellow hover:bg-duck-yellow/90 text-duck-teal font-bold text-lg transition-all duration-200 hover:scale-105 cursor-pointer disabled:opacity-50 disabled:hover:scale-100"
>
{isSubmitting ? 'Creating account...' : 'Sign Up'}
</Button>
</DialogFooter>
<p className="text-center text-sm text-duck-dark/60">
Already have an account?{' '}
<Link to="/auth/login" className="text-duck-forest underline hover:text-duck-forest/80">
Sign In
</Link>
</p>
</form>
</>
)}
</PaperDialog>
);
};
@@ -1,243 +0,0 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router';
import { toast } from 'sonner';
import { DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
import { PaperDialog } from '@/components/Dialogs';
import { useForm } from 'hooks/useForm';
import { useAuth } from 'hooks/useAuth';
import { useClient } from 'hooks/useClient';
type VerifyFormState = {
name?: string;
password?: string;
confirmPassword?: string;
};
const validateForm = (state: Partial<VerifyFormState>) => {
const { name, password, confirmPassword } = state;
if (!name || !password || !confirmPassword) return false;
if (password !== confirmPassword) return false;
return true;
};
type VerifyModalProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
};
type TokenStatus = 'loading' | 'valid' | 'invalid';
export const VerifyModal = ({ open, onOpenChange }: VerifyModalProps) => {
const navigate = useNavigate();
const client = useClient('/api/auth');
const { verify } = useAuth();
const { state, formRef, update, isValid } = useForm<VerifyFormState>({}, validateForm);
const [verificationCode] = useState(() => new URL(window.location.href).searchParams.get('verificationCode') ?? '');
const [tokenStatus, setTokenStatus] = useState<TokenStatus>('loading');
const [email, setEmail] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [verified, setVerified] = useState(false);
const [resending, setResending] = useState(false);
const [resent, setResent] = useState(false);
// Validate token on mount and strip it from the URL
useEffect(() => {
if (!open || !verificationCode) {
if (!verificationCode) setTokenStatus('invalid');
return;
}
window.history.replaceState(null, '', '/auth/verify');
client
.post<{ email: string }>('/verify-token', { verificationCode })
.then((data) => {
setEmail(data.email);
setTokenStatus('valid');
})
.catch(() => {
// Try to decode email from expired token for resend
try {
const payload = JSON.parse(atob(verificationCode.split('.')[1]!));
if (payload.email) setEmail(payload.email);
} catch {
// ignore
}
setTokenStatus('invalid');
});
}, [open]);
// Redirect after successful verification
useEffect(() => {
if (!verified) return;
const timer = setTimeout(() => navigate('/'), 3000);
return () => clearTimeout(timer);
}, [verified]);
const handleSubmit = async (ev: React.FormEvent) => {
ev.preventDefault();
if (!isValid || isSubmitting || !verificationCode) return;
setIsSubmitting(true);
try {
await verify({
verificationCode,
name: state.name,
password: state.password,
confirmPassword: state.confirmPassword,
});
setVerified(true);
} catch (ex) {
const error = ex as { message?: string };
toast.error(error.message || 'Verification failed. Please try again.');
update({ ...state, password: '', confirmPassword: '' });
} finally {
setIsSubmitting(false);
}
};
const handleResend = async () => {
if (!email || resending) return;
setResending(true);
try {
await client.post('/resend-verification', { email });
setResent(true);
} catch (ex) {
const error = ex as { message?: string };
toast.error(error.message || 'Failed to resend. Please try again.');
} finally {
setResending(false);
}
};
// Loading state
if (tokenStatus === 'loading') {
return (
<PaperDialog open={open} onOpenChange={onOpenChange}>
<div className="text-center py-4">
<DialogHeader>
<DialogTitle className="text-duck-dark text-2xl font-bold">Validating...</DialogTitle>
<DialogDescription className="text-duck-dark/60">Please wait</DialogDescription>
</DialogHeader>
</div>
</PaperDialog>
);
}
// Invalid / expired token
if (tokenStatus === 'invalid') {
return (
<PaperDialog open={open} onOpenChange={onOpenChange}>
<div className="text-center py-4">
<h2 className="text-2xl font-bold text-duck-dark">Link Expired</h2>
<p className="mt-4 text-duck-dark/70">
This verification link is no longer valid.{' '}
{email ? 'Click below to receive a new one.' : 'Please request a new one.'}
</p>
{resent ? (
<p className="mt-6 text-sm text-duck-teal font-medium">
A new verification email has been sent to <strong>{email}</strong>.
</p>
) : (
email && (
<Button
onClick={handleResend}
disabled={resending}
className="mt-6 bg-duck-yellow hover:bg-duck-yellow/90 text-duck-teal font-bold px-8 py-3 transition-all duration-200 hover:scale-105 cursor-pointer disabled:opacity-50"
>
{resending ? 'Sending...' : 'Resend Verification Email'}
</Button>
)
)}
</div>
</PaperDialog>
);
}
// Valid token — show form or success
return (
<PaperDialog
open={open}
onOpenChange={onOpenChange}
onOpenAutoFocus={(ev) => {
const firstInput = (ev.currentTarget as HTMLElement).querySelector('input:not([disabled])');
(firstInput as HTMLElement)?.focus();
}}
>
{verified ? (
<div className="text-center py-4">
<h2 className="text-2xl font-bold text-duck-dark">Account Verified</h2>
<p className="mt-4 text-duck-dark/70">
Congratulations! Your account has been set up successfully. Redirecting...
</p>
</div>
) : (
<>
<DialogHeader>
<DialogTitle className="text-duck-dark text-2xl font-bold">Set Up Your Account</DialogTitle>
<DialogDescription className="text-duck-dark/60">Complete your account details</DialogDescription>
</DialogHeader>
<form ref={formRef} onSubmit={handleSubmit} className="grid gap-4">
<Label className="grid gap-2">
<span className="text-duck-dark/70">Email</span>
<Input
className="h-11 bg-duck-dark/5 border-duck-dark/10 text-duck-dark disabled:opacity-100"
type="email"
value={email}
disabled
/>
</Label>
<Label className="grid gap-2">
<span className="text-duck-dark/70">Name</span>
<Input
className="h-11 bg-white/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
type="text"
name="name"
placeholder="Your name"
autoComplete="name"
/>
</Label>
<Label className="grid gap-2">
<span className="text-duck-dark/70">Password</span>
<Input
className="h-11 bg-white/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
type="password"
name="password"
placeholder="Min 12 chars, mixed case, number, symbol"
autoComplete="new-password"
/>
</Label>
<Label className="grid gap-2">
<span className="text-duck-dark/70">Confirm Password</span>
<Input
className="h-11 bg-white/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
type="password"
name="confirmPassword"
placeholder="Repeat your password"
autoComplete="new-password"
/>
</Label>
<DialogFooter className="pt-2">
<Button
type="submit"
disabled={!isValid || isSubmitting}
className="w-full h-11 bg-duck-yellow hover:bg-duck-yellow/90 text-duck-teal font-bold text-lg transition-all duration-200 hover:scale-105 cursor-pointer disabled:opacity-50 disabled:hover:scale-100"
>
{isSubmitting ? 'Verifying...' : 'Verify'}
</Button>
</DialogFooter>
</form>
</>
)}
</PaperDialog>
);
};
@@ -1,2 +0,0 @@
export { LandingPage } from './LandingPage';
export { AuthLayout } from './Layout';
+19
View File
@@ -0,0 +1,19 @@
declare module '*.jpg' {
const src: string;
export default src;
}
declare module '*.jpeg' {
const src: string;
export default src;
}
declare module '*.png' {
const src: string;
export default src;
}
declare module '*.svg' {
const src: string;
export default src;
}
-20
View File
@@ -1,23 +1,3 @@
declare module '*.jpg' {
const src: string;
export default src;
}
declare module '*.jpeg' {
const src: string;
export default src;
}
declare module '*.png' {
const src: string;
export default src;
}
declare module '*.svg' {
const src: string;
export default src;
}
declare global {
type SelectOption = { value: number | string; label?: string; href?: string; hidden?: boolean };