feat: forgot password flow, member page fixes, country name display
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m7s

Password reset:
- /forgot-password page: enter email, receive reset link via email
- /reset-password?token=xxx page: set new password with validation
- user.requestPasswordReset: generates token, sends styled email
- user.resetPassword: validates token, hashes new password
- Does NOT trigger re-onboarding — only resets the password
- 30-minute token expiry, cleared after use
- Added passwordResetToken/passwordResetExpiresAt to User model

Member detail page fixes:
- Hide "Expertise & Capacity" card for applicants/audience roles
- Show country names with flag emojis instead of raw ISO codes
- Login "Forgot password?" now links to /forgot-password page

Project detail page:
- Team member details show full country names with flags

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 13:49:43 +01:00
parent b6ba5d7145
commit ee8e90132e
10 changed files with 606 additions and 29 deletions

View File

@@ -0,0 +1,144 @@
'use client'
import { useState } from 'react'
import Link from 'next/link'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import { Mail, Loader2, CheckCircle2, AlertCircle, ArrowLeft } from 'lucide-react'
import { trpc } from '@/lib/trpc/client'
import { AnimatedCard } from '@/components/shared/animated-container'
export default function ForgotPasswordPage() {
const [email, setEmail] = useState('')
const [isSent, setIsSent] = useState(false)
const [error, setError] = useState<string | null>(null)
const requestReset = trpc.user.requestPasswordReset.useMutation({
onSuccess: () => {
setIsSent(true)
},
onError: (err) => {
setError(err.message || 'Something went wrong. Please try again.')
},
})
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError(null)
requestReset.mutate({ email: email.trim() })
}
if (isSent) {
return (
<AnimatedCard>
<Card className="w-full max-w-md overflow-hidden">
<div className="h-1 w-full bg-gradient-to-r from-brand-blue via-brand-teal to-brand-blue" />
<CardHeader className="text-center">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-emerald-50 animate-in zoom-in-50 duration-300">
<CheckCircle2 className="h-8 w-8 text-green-600" />
</div>
<CardTitle className="text-xl">Check your email</CardTitle>
<CardDescription className="text-base">
If an account exists for <strong>{email}</strong>, we&apos;ve sent a password reset link.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="rounded-lg border bg-muted/50 p-4 text-sm text-muted-foreground space-y-2">
<p>Click the link in the email to reset your password. The link will expire in 30 minutes.</p>
<p>If you don&apos;t see it, check your spam folder.</p>
</div>
<div className="border-t pt-4 space-y-2">
<Button
variant="outline"
className="w-full"
onClick={() => {
setIsSent(false)
setError(null)
}}
>
Try a different email
</Button>
<div className="text-center">
<Link href="/login" className="text-sm text-muted-foreground hover:text-primary transition-colors">
<ArrowLeft className="inline h-3.5 w-3.5 mr-1" />
Back to login
</Link>
</div>
</div>
</CardContent>
</Card>
</AnimatedCard>
)
}
return (
<AnimatedCard>
<Card className="w-full max-w-md overflow-hidden">
<div className="h-1 w-full bg-gradient-to-r from-brand-blue via-brand-teal to-brand-blue" />
<CardHeader className="text-center">
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-2xl bg-muted">
<Mail className="h-6 w-6 text-muted-foreground" />
</div>
<CardTitle className="text-xl">Reset your password</CardTitle>
<CardDescription>
Enter your email address and we&apos;ll send you a link to reset your password.
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<div className="flex items-center gap-2 rounded-md bg-destructive/10 p-3 text-sm text-destructive">
<AlertCircle className="h-4 w-4 shrink-0" />
<p>{error}</p>
</div>
)}
<div className="space-y-2">
<Label htmlFor="email">Email address</Label>
<Input
id="email"
type="email"
placeholder="you@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
disabled={requestReset.isPending}
autoComplete="email"
autoFocus
/>
</div>
<Button type="submit" className="w-full" disabled={requestReset.isPending || !email.trim()}>
{requestReset.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Sending...
</>
) : (
<>
<Mail className="mr-2 h-4 w-4" />
Send reset link
</>
)}
</Button>
<div className="text-center pt-2">
<Link href="/login" className="text-sm text-muted-foreground hover:text-primary transition-colors">
<ArrowLeft className="inline h-3.5 w-3.5 mr-1" />
Back to login
</Link>
</div>
</form>
</CardContent>
</Card>
</AnimatedCard>
)
}