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:
@@ -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'}
|
||||
|
||||
Reference in New Issue
Block a user