refactored Authentication
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card } from '@/components/Card';
|
||||
import { useForm } from 'hooks/useForm';
|
||||
|
||||
export function Bootstrap() {
|
||||
const authClient = useClient('/api/auth');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [done, setDone] = useState(false);
|
||||
const { formRef, state, isValid } = useForm({}, (state) => !!state.email);
|
||||
|
||||
const handleBootstrap = async () => {
|
||||
const trimmed = state.email.trim();
|
||||
if (!trimmed || isSubmitting) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
await authClient.post('/bootstrap', { 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 (
|
||||
<Card>
|
||||
{done ? (
|
||||
<div className="text-center">
|
||||
<h2 className="text-2xl font-bold text-duck-dark mb-2">Check your email</h2>
|
||||
<div className="text-sm text-duck-dark/60">
|
||||
<p>A verification link has been sent to <strong>{state.email}</strong>.</p>
|
||||
<p>Click it to activate your account.</p>
|
||||
</div>
|
||||
</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>
|
||||
<form ref={formRef} className="flex gap-2">
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
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={!isValid || 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>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { OfficerSvgLogo } from '@/components/Logos';
|
||||
import { useLandingPage } from '@/state/useLandingPage';
|
||||
import { Bootstrap } from './Bootstrap';
|
||||
import { Login } from './Login';
|
||||
|
||||
export function LandingPage() {
|
||||
const { registrationOpen, isLoading } = useLandingPage();
|
||||
if (isLoading) return null;
|
||||
|
||||
return (
|
||||
<div className="relative overflow-hidden h-dvh outline-none inset-0">
|
||||
<header className="pt-12 md:pt-20 lg:pt-24 xl:pt-0 z-20 flex justify-center">
|
||||
<div className="hidden md:block md:pt-28">
|
||||
<OfficerSvgLogo size="2xl" />
|
||||
</div>
|
||||
<div className="block md:hidden">
|
||||
<OfficerSvgLogo size="lg" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="absolute inset-x-0 bottom-0 z-20 flex justify-center pb-0 md:pb-12">
|
||||
{registrationOpen ? <Bootstrap /> : <Login />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Link } from 'react-router';
|
||||
import { Card } from '@/components/Card';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { useForm } from 'hooks/useForm';
|
||||
import { useAuth } from 'hooks/useAuth';
|
||||
|
||||
export function Login() {
|
||||
const navigate = useNavigate();
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="md:py-12 md:px-24 flex flex-col gap-6">
|
||||
<div className="text-center">
|
||||
<div className="text-duck-dark text-2xl font-bold">Welcome Back</div>
|
||||
<div className="text-duck-dark/60">Sign in to your account</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<div 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 ? 'Signing in...' : 'Sign In'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
type LoginFormState = {
|
||||
email?: string;
|
||||
password?: string;
|
||||
};
|
||||
|
||||
const validateForm = (state: Partial<LoginFormState>) => {
|
||||
const { email, password } = state;
|
||||
if (!email || !password) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './LandingPage';
|
||||
Reference in New Issue
Block a user