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

@@ -62,10 +62,10 @@ import {
ThumbsDown,
Globe,
Building2,
Flag,
FileText,
FolderOpen,
} from 'lucide-react'
import { getCountryName, getCountryFlag } from '@/lib/countries'
export default function MemberDetailPage() {
const params = useParams()
@@ -266,19 +266,19 @@ export default function MemberDetailPage() {
<div className="grid gap-4 sm:grid-cols-2">
{user.nationality && (
<div className="flex items-start gap-2">
<Flag className="h-4 w-4 mt-0.5 text-muted-foreground shrink-0" />
<span className="text-lg mt-0.5 shrink-0" role="img">{getCountryFlag(user.nationality)}</span>
<div>
<p className="text-xs font-medium text-muted-foreground">Nationality</p>
<p className="text-sm">{user.nationality}</p>
<p className="text-sm">{getCountryName(user.nationality)}</p>
</div>
</div>
)}
{user.country && (
<div className="flex items-start gap-2">
<Globe className="h-4 w-4 mt-0.5 text-muted-foreground shrink-0" />
<span className="text-lg mt-0.5 shrink-0" role="img">{getCountryFlag(user.country)}</span>
<div>
<p className="text-xs font-medium text-muted-foreground">Country of Residence</p>
<p className="text-sm">{user.country}</p>
<p className="text-sm">{getCountryName(user.country)}</p>
</div>
</div>
)}
@@ -447,7 +447,8 @@ export default function MemberDetailPage() {
</CardContent>
</Card>
{/* Expertise & Capacity */}
{/* Expertise & Capacity — only for jury/mentor/observer/admin roles */}
{!['APPLICANT', 'AUDIENCE'].includes(user.role) && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
@@ -494,6 +495,7 @@ export default function MemberDetailPage() {
)}
</CardContent>
</Card>
)}
</div>
{/* Mentor Assignments Section */}

View File

@@ -77,6 +77,7 @@ import {
} from 'lucide-react'
import { toast } from 'sonner'
import { formatDateOnly } from '@/lib/utils'
import { getCountryName, getCountryFlag } from '@/lib/countries'
interface PageProps {
params: Promise<{ id: string }>
@@ -517,7 +518,11 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
const isLastLead =
member.role === 'LEAD' &&
project.teamMembers.filter((m: { role: string }) => m.role === 'LEAD').length <= 1
const details = [member.user.nationality, member.user.institution, member.user.country].filter(Boolean)
const details = [
member.user.nationality ? `${getCountryFlag(member.user.nationality)} ${getCountryName(member.user.nationality)}` : null,
member.user.institution,
member.user.country && member.user.country !== member.user.nationality ? `${getCountryFlag(member.user.country)} ${getCountryName(member.user.country)}` : null,
].filter(Boolean)
return (
<div key={member.id} className="flex items-center gap-3 p-3 rounded-lg border">
{member.role === 'LEAD' ? (