Files
Curio/src/app/auth/reset-password/page.tsx
T
arnaudne 5bf6013460 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>
2026-07-08 22:08:14 +02:00

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;