Authentication reaches zero -- sign-in, bootstrap, forgot and reset password. These are the first words a new account reads, which is why the brief ranked them above the larger screens. DavAppPasswords had the third toLocaleDateString(undefined, ...) in the shell; it now uses useFormat().date like the other two. DebugPanel's three labels are keyed too. Worth knowing rather than acting on: that panel is rendered behind showDebug, which Background.tsx hardcodes to false, so it never appears. Keyed rather than deleted because removing it is not my call.
114 lines
4.2 KiB
TypeScript
114 lines
4.2 KiB
TypeScript
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 { useState } from 'react';
|
|
import { toast } from 'sonner';
|
|
import { useForm } from 'hooks/useForm';
|
|
import { useAuth } from 'hooks/useAuth';
|
|
import { useGlobal } from 'hooks/useGlobal';
|
|
import { useTranslation } from '@/lib/i18n';
|
|
|
|
const initialState: LoginFormState = {
|
|
// email: 'pastilhas@pastilhas.dev',
|
|
// password: '',
|
|
};
|
|
export function Login() {
|
|
const { t } = useTranslation();
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const { state, formRef, update, isValid } = useForm<LoginFormState>(initialState, validateForm);
|
|
const { signin } = useAuth();
|
|
const [, setDuckHidden] = useGlobal('DUCK_HIDDEN', false);
|
|
|
|
const handleSubmit = async (ev: React.FormEvent) => {
|
|
ev.preventDefault();
|
|
if (!isValid || isSubmitting) return;
|
|
|
|
setIsSubmitting(true);
|
|
setDuckHidden(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-6 md:px-24 md:min-w-[480px] flex flex-col gap-6">
|
|
<div className="text-center">
|
|
<div className="text-duck-dark text-2xl font-bold">{t('auth.signInTitle')}</div>
|
|
<div className="text-duck-dark/60">{t('auth.signInSubtitle')}</div>
|
|
</div>
|
|
|
|
<form ref={formRef} onSubmit={handleSubmit} className="grid gap-4">
|
|
<Label className="grid gap-2">
|
|
<span className="text-duck-dark/70">{t('auth.emailOrUsername')}</span>
|
|
<Input
|
|
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
|
|
// `text`, not `email`: the browser refuses to submit a username under type="email", so the
|
|
// field would have validated the user out of an account they have.
|
|
type="text"
|
|
name="email"
|
|
placeholder={t('auth.emailPlaceholder')}
|
|
// `username` rather than `email`, which is what password managers expect for a field that
|
|
// takes either — and it is the value they already store beside the password.
|
|
autoComplete="username"
|
|
/>
|
|
</Label>
|
|
|
|
<Label className="grid gap-2">
|
|
<span className="text-duck-dark/70">{t('auth.password')}</span>
|
|
<Input
|
|
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
|
|
type="password"
|
|
name="password"
|
|
placeholder={t('auth.passwordPlaceholder')}
|
|
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">
|
|
{t('auth.forgotPassword')}
|
|
</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;
|
|
};
|