Add image cropping to avatar upload and show avatars platform-wide

- Add react-easy-crop for circular crop + zoom UI on avatar upload
- Create server-side getUserAvatarUrl utility for generating pre-signed URLs
- Update all nav components (admin, jury, mentor, observer) to show user avatars
- Add avatar URLs to user list, mentor list, and project detail API responses
- Replace initials-only avatars with UserAvatar component across admin pages

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-02 13:19:28 +01:00
parent f9f88d68ab
commit 8fda8deded
14 changed files with 346 additions and 140 deletions

View File

@@ -26,7 +26,7 @@ import {
import { FileViewer } from '@/components/shared/file-viewer'
import { FileUpload } from '@/components/shared/file-upload'
import { ProjectLogoWithUrl } from '@/components/shared/project-logo-with-url'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import { UserAvatar } from '@/components/shared/user-avatar'
import {
ArrowLeft,
Edit,
@@ -47,7 +47,7 @@ import {
Crown,
UserPlus,
} from 'lucide-react'
import { formatDate, formatDateOnly, getInitials } from '@/lib/utils'
import { formatDate, formatDateOnly } from '@/lib/utils'
interface PageProps {
params: Promise<{ id: string }>
@@ -360,17 +360,15 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
</CardHeader>
<CardContent>
<div className="grid gap-3 sm:grid-cols-2">
{project.teamMembers.map((member: { id: string; role: string; title: string | null; user: { id: string; name: string | null; email: string } }) => (
{project.teamMembers.map((member: { id: string; role: string; title: string | null; user: { id: string; name: string | null; email: string; avatarUrl?: string | null } }) => (
<div key={member.id} className="flex items-center gap-3 p-3 rounded-lg border">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-muted">
{member.role === 'LEAD' ? (
{member.role === 'LEAD' ? (
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-muted">
<Crown className="h-5 w-5 text-yellow-500" />
) : (
<span className="text-sm font-medium">
{getInitials(member.user.name || member.user.email)}
</span>
)}
</div>
</div>
) : (
<UserAvatar user={member.user} avatarUrl={member.user.avatarUrl} size="md" />
)}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<p className="font-medium text-sm truncate">
@@ -417,11 +415,11 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
{project.mentorAssignment ? (
<div className="flex items-center justify-between p-3 rounded-lg border">
<div className="flex items-center gap-3">
<Avatar className="h-10 w-10">
<AvatarFallback className="text-sm">
{getInitials(project.mentorAssignment.mentor.name || project.mentorAssignment.mentor.email)}
</AvatarFallback>
</Avatar>
<UserAvatar
user={project.mentorAssignment.mentor}
avatarUrl={project.mentorAssignment.mentor.avatarUrl}
size="md"
/>
<div>
<p className="font-medium">
{project.mentorAssignment.mentor.name || 'Unnamed'}
@@ -519,11 +517,11 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
<TableRow key={assignment.id}>
<TableCell>
<div className="flex items-center gap-2">
<Avatar className="h-8 w-8">
<AvatarFallback className="text-xs">
{getInitials(assignment.user.name || assignment.user.email)}
</AvatarFallback>
</Avatar>
<UserAvatar
user={assignment.user}
avatarUrl={assignment.user.avatarUrl}
size="sm"
/>
<div>
<p className="font-medium text-sm">
{assignment.user.name || 'Unnamed'}