feat: add password reset email

This commit is contained in:
gosangam
2026-06-02 18:37:38 +05:30
parent dc01d09f09
commit cb70a84dd5
12 changed files with 662 additions and 88 deletions
+70 -4
View File
@@ -171,10 +171,16 @@ export function ProfileEditForm({
<div className="space-y-1.5">
<Label htmlFor="profile-email">Email</Label>
<Input id="profile-email" value={user.email} disabled />
<p className="text-xs text-muted-foreground">
Email changes aren&apos;t supported yet.
</p>
{user.email ? (
<>
<Input id="profile-email" value={user.email} disabled />
<p className="text-xs text-muted-foreground">
Email changes aren&apos;t supported yet.
</p>
</>
) : (
<AddEmailInline />
)}
</div>
</div>
@@ -202,6 +208,66 @@ export function ProfileEditForm({
);
}
function AddEmailInline() {
const { setUser } = useAuth();
const [email, setEmail] = useState("");
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [sent, setSent] = useState(false);
async function onSave(e: React.FormEvent) {
e.preventDefault();
setError(null);
setSaving(true);
try {
const updated = await auth.setEmail(email);
setUser(updated);
setSent(true);
} catch (err) {
setError(err instanceof AuthError ? err.message : "Could not save email.");
} finally {
setSaving(false);
}
}
if (sent) {
return (
<p className="text-xs text-muted-foreground">
✓ Email saved — check your inbox for a verification link.
</p>
);
}
return (
<form onSubmit={onSave} className="space-y-1.5">
<div className="flex gap-2">
<Input
id="profile-email"
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
/>
<Button
type="submit"
disabled={saving || email.trim() === ""}
className="h-10 px-3 text-sm"
>
{saving ? "Saving…" : "Save"}
</Button>
</div>
{error ? (
<p className="text-xs text-destructive">{error}</p>
) : (
<p className="text-xs text-muted-foreground">
Add an email to enable password recovery and notifications.
</p>
)}
</form>
);
}
function AvatarPreview({ src, name }: { src: string | null; name: string }) {
if (src) {
return (
+5 -5
View File
@@ -12,8 +12,8 @@
* "is this my profile?" signal (the /u/me layout passes it; the
* /u/[handle] layout does not), so the two buttons stay in lockstep
* and visitors on someone else's profile see a clean row with no
* personal-settings affordances. The gear links to /u/me/settings
* (the inline reading-prefs page) rather than opening a popover —
* personal-settings affordances. The gear links to /settings (the
* dedicated settings page) rather than opening a popover —
* centred-row popovers tend to spill off the left edge on narrow
* viewports.
*
@@ -84,9 +84,9 @@ export function ProfileHeader({
) : null}
{showReadingSettings ? (
<Link
href="/u/me/settings"
aria-label="Reading settings"
title="Reading settings"
href="/settings"
aria-label="Settings"
title="Settings"
className={cn(
"size-9 inline-flex items-center justify-center rounded-lg",
"border border-border text-foreground hover:bg-muted",