feat: full feature buildout — streaming, i18n, mastery map, admin, jobs
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>
This commit is contained in:
@@ -2,9 +2,11 @@
|
||||
|
||||
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();
|
||||
@@ -16,15 +18,15 @@ export default function ResetPasswordPage() {
|
||||
if (!token) {
|
||||
return (
|
||||
<main style={s.page}>
|
||||
<p style={{ color: 'var(--color-misconception)' }}>Invalid reset link.</p>
|
||||
<Link href="/auth/forgot-password" style={s.link}>Request a new one</Link>
|
||||
<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('Passwords do not match.'); return; }
|
||||
if (password !== confirm) { setError(t('passwordMismatch')); return; }
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const res = await fetch('/api/auth/reset-password', {
|
||||
@@ -37,21 +39,21 @@ export default function ResetPasswordPage() {
|
||||
router.push('/auth/login?reset=1');
|
||||
} else {
|
||||
const d = (await res.json()) as { error?: string };
|
||||
setError(d.error ?? 'Failed to reset password.');
|
||||
setError(d.error ?? t('error'));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<main style={s.page}>
|
||||
<h1 style={s.heading}>Set new password</h1>
|
||||
<h1 style={s.heading}>{t('title')}</h1>
|
||||
<form onSubmit={handleSubmit} style={s.form} noValidate>
|
||||
<label style={s.label} htmlFor="password">New password</label>
|
||||
<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">Confirm password</label>
|
||||
<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 ? 'Saving…' : 'Set password'}
|
||||
{loading ? t('submitting') : t('submit')}
|
||||
</button>
|
||||
</form>
|
||||
</main>
|
||||
@@ -65,6 +67,6 @@ const s = {
|
||||
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: '#fff', border: 'none', borderRadius: '4px', fontFamily: 'var(--font-display)', fontSize: '0.9375rem', fontWeight: 500, cursor: 'pointer' },
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user