feat: member profile pages with clickable links from all member lists
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m51s
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m51s
- Member detail page (/admin/members/[id]) now shows: - Profile details card (nationality, country, institution, bio) - Team memberships / projects with links to project pages - Jury groups with role (Chair/Member/Observer) - All roles including Applicant, Award Master, Audience in role selector - Project detail page team members now show: - Nationality, institution, country inline - Names are clickable links to member profile pages - Members list: names are clickable links to profile pages (all tabs) - Applicants tab: added nationality and institution columns - Backend: user.get includes teamMemberships and juryGroupMemberships - Backend: project.getFullDetail includes nationality/country/institution Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -60,6 +60,11 @@ import {
|
||||
Eye,
|
||||
ThumbsUp,
|
||||
ThumbsDown,
|
||||
Globe,
|
||||
Building2,
|
||||
Flag,
|
||||
FileText,
|
||||
FolderOpen,
|
||||
} from 'lucide-react'
|
||||
|
||||
export default function MemberDetailPage() {
|
||||
@@ -116,7 +121,7 @@ export default function MemberDetailPage() {
|
||||
id: userId,
|
||||
email: email || undefined,
|
||||
name: name || null,
|
||||
role: role as 'SUPER_ADMIN' | 'JURY_MEMBER' | 'MENTOR' | 'OBSERVER' | 'PROGRAM_ADMIN',
|
||||
role: role as 'SUPER_ADMIN' | 'PROGRAM_ADMIN' | 'AWARD_MASTER' | 'JURY_MEMBER' | 'MENTOR' | 'OBSERVER' | 'APPLICANT' | 'AUDIENCE',
|
||||
status: status as 'NONE' | 'INVITED' | 'ACTIVE' | 'SUSPENDED',
|
||||
expertiseTags,
|
||||
maxAssignments: maxAssignments ? parseInt(maxAssignments) : null,
|
||||
@@ -247,6 +252,123 @@ export default function MemberDetailPage() {
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="profile" className="space-y-6">
|
||||
{/* Profile Details (read-only) */}
|
||||
{(user.nationality || user.country || user.institution || user.bio) && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Globe className="h-5 w-5" />
|
||||
Profile Details
|
||||
</CardTitle>
|
||||
<CardDescription>Information provided during onboarding</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<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" />
|
||||
<div>
|
||||
<p className="text-xs font-medium text-muted-foreground">Nationality</p>
|
||||
<p className="text-sm">{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" />
|
||||
<div>
|
||||
<p className="text-xs font-medium text-muted-foreground">Country of Residence</p>
|
||||
<p className="text-sm">{user.country}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{user.institution && (
|
||||
<div className="flex items-start gap-2">
|
||||
<Building2 className="h-4 w-4 mt-0.5 text-muted-foreground shrink-0" />
|
||||
<div>
|
||||
<p className="text-xs font-medium text-muted-foreground">Institution / Organization</p>
|
||||
<p className="text-sm">{user.institution}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{user.bio && (
|
||||
<div className="flex items-start gap-2 sm:col-span-2">
|
||||
<FileText className="h-4 w-4 mt-0.5 text-muted-foreground shrink-0" />
|
||||
<div>
|
||||
<p className="text-xs font-medium text-muted-foreground">Bio</p>
|
||||
<p className="text-sm whitespace-pre-line">{user.bio}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Team Memberships / Projects */}
|
||||
{user.teamMemberships && user.teamMemberships.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FolderOpen className="h-5 w-5" />
|
||||
Projects ({user.teamMemberships.length})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-2">
|
||||
{user.teamMemberships.map((tm) => (
|
||||
<Link
|
||||
key={tm.id}
|
||||
href={`/admin/projects/${tm.project.id}`}
|
||||
className="flex items-center justify-between p-3 rounded-lg border hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<p className="font-medium text-sm truncate">{tm.project.title}</p>
|
||||
{tm.project.teamName && (
|
||||
<p className="text-xs text-muted-foreground">{tm.project.teamName}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0 ml-2">
|
||||
{tm.project.competitionCategory && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{tm.project.competitionCategory.replace('_', ' ')}
|
||||
</Badge>
|
||||
)}
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{tm.role === 'LEAD' ? 'Lead' : tm.role === 'ADVISOR' ? 'Advisor' : 'Member'}
|
||||
</Badge>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Jury Groups (for jury members) */}
|
||||
{user.juryGroupMemberships && user.juryGroupMemberships.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Shield className="h-5 w-5" />
|
||||
Jury Groups ({user.juryGroupMemberships.length})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{user.juryGroupMemberships.map((m: { id: string; role: string; juryGroup: { id: string; name: string } }) => (
|
||||
<Badge key={m.id} variant="outline" className="text-sm py-1 px-3">
|
||||
{m.juryGroup.name}
|
||||
<span className="ml-1.5 text-xs text-muted-foreground">
|
||||
({m.role === 'CHAIR' ? 'Chair' : m.role === 'OBSERVER' ? 'Observer' : 'Member'})
|
||||
</span>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{/* Basic Info */}
|
||||
<Card>
|
||||
@@ -302,6 +424,9 @@ export default function MemberDetailPage() {
|
||||
<SelectItem value="JURY_MEMBER">Jury Member</SelectItem>
|
||||
<SelectItem value="MENTOR">Mentor</SelectItem>
|
||||
<SelectItem value="OBSERVER">Observer</SelectItem>
|
||||
<SelectItem value="APPLICANT">Applicant</SelectItem>
|
||||
<SelectItem value="AWARD_MASTER">Award Master</SelectItem>
|
||||
<SelectItem value="AUDIENCE">Audience</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user