'use client'; /** * Password gate — shown when accessing a protected album/subpage. * Submits to /api/auth and refreshes on success. */ import { useState } from 'react'; import Link from 'next/link'; import styles from './PasswordGate.module.css'; import { useDictionary } from './I18nProvider'; interface PasswordGateProps { slug: string; title: string; type?: 'subpage' | 'album' | 'journal' | 'page' | 'site'; /** * Links shown under the card. The site gate passes its legal pages here, * which a locked site still serves (proxy.ts, UNGATED_PAGES). */ legalLinks?: { href: string; label: string }[]; } /** Whole seconds from a `Retry-After` header, or null when there is none. */ function retryAfterSeconds(res: Response): number | null { const seconds = Number.parseInt(res.headers.get('retry-after') ?? '', 10); return Number.isFinite(seconds) && seconds > 0 ? seconds : null; } export default function PasswordGate({ slug, title, type = 'subpage', legalLinks = [], }: PasswordGateProps) { const t = useDictionary(); const [password, setPassword] = useState(''); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); setError(''); setLoading(true); try { const res = await fetch('/api/auth', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ slug, password, type }), }); if (res.ok) { window.location.reload(); } else if (res.status === 401) { setError(t.password.incorrect); setPassword(''); } else if (res.status === 429) { // The limit says nothing about the password, which may well be right: // keep it in the field and say when the next attempt will be heard. setError(t.password.tooManyAttempts(retryAfterSeconds(res))); } else { setError(t.password.failed); } } catch { setError(t.password.failed); } finally { setLoading(false); } } return (

{title}

{type === 'site' ? t.password.siteSubtitle : type === 'page' ? t.password.pageSubtitle : type === 'journal' ? t.password.journalSubtitle : t.password.subtitle}

setPassword(e.target.value)} placeholder={t.password.placeholder} aria-label={t.password.placeholder} className={styles.input} autoFocus required disabled={loading} aria-invalid={!!error} aria-describedby={error ? 'password-error' : undefined} />
{error && ( )}
{legalLinks.length > 0 && ( )}
); }