5bf6013460
Progressive lesson streaming via onSegment callback (fixes SSE for non-English users — locale was shadowed in lesson-reader useEffect). Adds: BullMQ workers, Redis stream buffer, token budget enforcement, Langfuse tracing, golden-eval runner, Playwright e2e scaffolding, lesson depth/locale/preferences schema, mastery map UI, admin panel (blueprints/users/reports/quality/misconceptions), image queries, source citations, view transitions, reading animations, i18n (next-intl), PDF export, surprise endpoint, and 402 passing unit tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
73 lines
3.6 KiB
TypeScript
73 lines
3.6 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { useSearchParams, useRouter } from 'next/navigation';
|
|
import { useTranslations } from 'next-intl';
|
|
import Link from 'next/link';
|
|
|
|
export default function ResetPasswordPage() {
|
|
const t = useTranslations('auth.resetPassword');
|
|
const params = useSearchParams();
|
|
const token = params.get('token') ?? '';
|
|
const router = useRouter();
|
|
const [password, setPassword] = useState('');
|
|
const [confirm, setConfirm] = useState('');
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
if (!token) {
|
|
return (
|
|
<main style={s.page}>
|
|
<p style={{ color: 'var(--color-misconception)' }}>{t('invalidLink')}</p>
|
|
<Link href="/auth/forgot-password" style={s.link}>{t('requestNew')}</Link>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (password !== confirm) { setError(t('passwordMismatch')); return; }
|
|
setLoading(true);
|
|
setError(null);
|
|
const res = await fetch('/api/auth/reset-password', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ token, password }),
|
|
});
|
|
setLoading(false);
|
|
if (res.ok) {
|
|
router.push('/auth/login?reset=1');
|
|
} else {
|
|
const d = (await res.json()) as { error?: string };
|
|
setError(d.error ?? t('error'));
|
|
}
|
|
};
|
|
|
|
return (
|
|
<main style={s.page}>
|
|
<h1 style={s.heading}>{t('title')}</h1>
|
|
<form onSubmit={handleSubmit} style={s.form} noValidate>
|
|
<label style={s.label} htmlFor="password">{t('newPassword')}</label>
|
|
<input id="password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} required minLength={8} autoComplete="new-password" style={s.input} />
|
|
<label style={s.label} htmlFor="confirm">{t('confirmPassword')}</label>
|
|
<input id="confirm" type="password" value={confirm} onChange={(e) => setConfirm(e.target.value)} required autoComplete="new-password" style={s.input} />
|
|
{error && <p role="alert" style={s.error}>{error}</p>}
|
|
<button type="submit" disabled={!password || !confirm || loading} style={s.btn}>
|
|
{loading ? t('submitting') : t('submit')}
|
|
</button>
|
|
</form>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
const s = {
|
|
page: { display: 'flex', flexDirection: 'column' as const, alignItems: 'center', minHeight: '100svh', padding: 'var(--space-8)', paddingTop: 'var(--space-16)', gap: 'var(--space-4)' },
|
|
heading: { fontFamily: 'var(--font-display)', fontSize: '1.5rem', fontWeight: 500, color: 'var(--color-ink)', marginBottom: 'var(--space-4)' },
|
|
form: { width: '100%', maxWidth: '26rem', display: 'flex', flexDirection: 'column' as const, gap: 'var(--space-2)' },
|
|
label: { fontFamily: 'var(--font-display)', fontSize: '0.875rem', fontWeight: 500, color: 'var(--color-ink)', marginTop: 'var(--space-3)' },
|
|
input: { padding: 'var(--space-3) var(--space-4)', background: 'var(--color-surface)', border: '1px solid var(--color-border)', borderRadius: '4px', fontFamily: 'var(--font-body)', fontSize: '1rem', color: 'var(--color-ink)', outline: 'none' },
|
|
error: { color: 'var(--color-misconception)', fontSize: '0.875rem', margin: 0 },
|
|
btn: { marginTop: 'var(--space-4)', padding: 'var(--space-3) var(--space-6)', background: 'var(--color-accent)', color: 'var(--color-on-accent)', border: 'none', borderRadius: '4px', fontFamily: 'var(--font-display)', fontSize: '0.9375rem', fontWeight: 500, cursor: 'pointer' },
|
|
link: { color: 'var(--color-accent)', fontFamily: 'var(--font-display)', fontSize: '0.875rem', textDecoration: 'none' },
|
|
} as const;
|