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

@@ -2,6 +2,7 @@ import { z } from 'zod'
import { TRPCError } from '@trpc/server'
import { Prisma } from '@prisma/client'
import { router, protectedProcedure, adminProcedure } from '../trpc'
import { getUserAvatarUrl } from '../utils/avatar-url'
export const projectRouter = router({
/**
@@ -91,7 +92,7 @@ export const projectRouter = router({
teamMembers: {
include: {
user: {
select: { id: true, name: true, email: true },
select: { id: true, name: true, email: true, profileImageKey: true, profileImageProvider: true },
},
},
orderBy: { joinedAt: 'asc' },
@@ -99,7 +100,7 @@ export const projectRouter = router({
mentorAssignment: {
include: {
mentor: {
select: { id: true, name: true, email: true, expertiseTags: true },
select: { id: true, name: true, email: true, expertiseTags: true, profileImageKey: true, profileImageProvider: true },
},
},
},
@@ -123,7 +124,35 @@ export const projectRouter = router({
}
}
return project
// Attach avatar URLs to team members and mentor
const teamMembersWithAvatars = await Promise.all(
project.teamMembers.map(async (member) => ({
...member,
user: {
...member.user,
avatarUrl: await getUserAvatarUrl(member.user.profileImageKey, member.user.profileImageProvider),
},
}))
)
const mentorWithAvatar = project.mentorAssignment
? {
...project.mentorAssignment,
mentor: {
...project.mentorAssignment.mentor,
avatarUrl: await getUserAvatarUrl(
project.mentorAssignment.mentor.profileImageKey,
project.mentorAssignment.mentor.profileImageProvider
),
},
}
: null
return {
...project,
teamMembers: teamMembersWithAvatars,
mentorAssignment: mentorWithAvatar,
}
}),
/**