107 lines
4.0 KiB
TypeScript
107 lines
4.0 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { getBrowserSupabase } from '@/lib/supabase';
|
|
import Wordmark from '@/components/Wordmark';
|
|
|
|
export default function ForgotPasswordPage() {
|
|
const [email, setEmail] = useState('');
|
|
const [busy, setBusy] = useState(false);
|
|
const [sent, setSent] = useState(false);
|
|
const [error, setError] = useState('');
|
|
|
|
const request = async (e?: React.FormEvent) => {
|
|
e?.preventDefault();
|
|
setError('');
|
|
if (!email) return setError('Enter your email.');
|
|
setBusy(true);
|
|
const supabase = getBrowserSupabase();
|
|
if (!supabase) {
|
|
setBusy(false);
|
|
return setError('Auth is not configured.');
|
|
}
|
|
const { error: err } = await supabase.auth.resetPasswordForEmail(email, {
|
|
redirectTo: `${window.location.origin}/auth/callback?next=/profile`,
|
|
});
|
|
setBusy(false);
|
|
if (err) return setError(err.message);
|
|
setSent(true);
|
|
};
|
|
|
|
return (
|
|
<section style={{ minHeight: '80vh', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '32px 16px' }}>
|
|
<div className="surface diagonal-cut animate-fade-up" style={{ width: '100%', maxWidth: 420, padding: 32 }}>
|
|
<a
|
|
href="/"
|
|
style={{ display: 'flex', justifyContent: 'center', color: 'var(--text-0)', textDecoration: 'none', marginBottom: 24 }}
|
|
aria-label="VYNDR — home"
|
|
>
|
|
<Wordmark size={26} />
|
|
</a>
|
|
|
|
{!sent ? (
|
|
<>
|
|
<h1 style={{ fontSize: 22, fontWeight: 700, textAlign: 'center', marginBottom: 6 }}>Reset your password</h1>
|
|
<p style={{ textAlign: 'center', color: 'var(--text-1)', fontSize: 13, marginBottom: 24 }}>
|
|
Enter your email. We'll send you a reset link.
|
|
</p>
|
|
<form onSubmit={request} style={{ display: 'grid', gap: 12 }}>
|
|
<div>
|
|
<label className="lbl" style={{ display: 'block', marginBottom: 6 }}>Email</label>
|
|
<input
|
|
type="email"
|
|
required
|
|
autoComplete="email"
|
|
className="input-field"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
/>
|
|
</div>
|
|
{error && <p style={{ color: 'var(--grade-d)', fontSize: 13, margin: 0 }}>{error}</p>}
|
|
<button type="submit" disabled={busy} className="btn-primary" style={{ padding: 14, marginTop: 4 }}>
|
|
{busy ? 'Sending…' : 'Send Reset Link'}
|
|
</button>
|
|
</form>
|
|
</>
|
|
) : (
|
|
<ConfirmationBlock email={email} onResend={request} busy={busy} />
|
|
)}
|
|
|
|
<p style={{ textAlign: 'center', fontSize: 13, color: 'var(--text-1)', marginTop: 20 }}>
|
|
<a href="/login" style={{ color: 'var(--text-1)' }}>← Back to sign in</a>
|
|
</p>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function ConfirmationBlock({ email, onResend, busy }: { email: string; onResend: () => void; busy: boolean }) {
|
|
return (
|
|
<div style={{ textAlign: 'center', display: 'grid', gap: 8, justifyItems: 'center' }}>
|
|
<EnvelopeIcon />
|
|
<h2 style={{ fontSize: 20, fontWeight: 700 }}>Check your inbox</h2>
|
|
<p style={{ color: 'var(--text-1)', fontSize: 14, maxWidth: 320 }}>
|
|
We sent a reset link to <span className="mono" style={{ color: 'var(--text-0)' }}>{email}</span>. It expires in 1 hour.
|
|
</p>
|
|
<button
|
|
type="button"
|
|
onClick={onResend}
|
|
disabled={busy}
|
|
className="btn-ghost"
|
|
style={{ marginTop: 8, fontSize: 13 }}
|
|
>
|
|
{busy ? 'Resending…' : "Didn't get it? Resend"}
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function EnvelopeIcon() {
|
|
return (
|
|
<svg width="56" height="56" viewBox="0 0 24 24" fill="none" aria-hidden>
|
|
<rect x="3" y="5" width="18" height="14" rx="2" stroke="var(--grade-a)" strokeWidth="1.5" />
|
|
<path d="M3 7l9 6 9-6" stroke="var(--grade-a)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
</svg>
|
|
);
|
|
}
|