Add user tag editing and improve member error display
Build and Push Docker Image / build (push) Successful in 9m4s Details

- Display actual error message in member detail page instead of generic Member not found
- Add debug logging to user.get query to help diagnose issues
- Add expertise tags editing for users in profile settings page
- Update user.updateProfile mutation to accept expertiseTags

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Matt 2026-02-05 13:45:34 +01:00
parent f59cfd393b
commit d7f0118940
3 changed files with 67 additions and 12 deletions

View File

@ -50,7 +50,7 @@ export default function MemberDetailPage() {
const router = useRouter() const router = useRouter()
const userId = params.id as string const userId = params.id as string
const { data: user, isLoading, refetch } = trpc.user.get.useQuery({ id: userId }) const { data: user, isLoading, error, refetch } = trpc.user.get.useQuery({ id: userId })
const updateUser = trpc.user.update.useMutation() const updateUser = trpc.user.update.useMutation()
const sendInvitation = trpc.user.sendInvitation.useMutation() const sendInvitation = trpc.user.sendInvitation.useMutation()
@ -121,14 +121,19 @@ export default function MemberDetailPage() {
) )
} }
if (!user) { if (error || !user) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<Alert variant="destructive"> <Alert variant="destructive">
<AlertCircle className="h-4 w-4" /> <AlertCircle className="h-4 w-4" />
<AlertTitle>Member not found</AlertTitle> <AlertTitle>Error Loading Member</AlertTitle>
<AlertDescription> <AlertDescription>
The member you&apos;re looking for does not exist. {error?.message || 'The member you\'re looking for does not exist.'}
{process.env.NODE_ENV === 'development' && (
<div className="mt-2 text-xs opacity-75">
User ID: {userId}
</div>
)}
</AlertDescription> </AlertDescription>
</Alert> </Alert>
<Button asChild> <Button asChild>

View File

@ -35,6 +35,7 @@ import {
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { AvatarUpload } from '@/components/shared/avatar-upload' import { AvatarUpload } from '@/components/shared/avatar-upload'
import { UserAvatar } from '@/components/shared/user-avatar' import { UserAvatar } from '@/components/shared/user-avatar'
import { TagInput } from '@/components/shared/tag-input'
import { import {
Loader2, Loader2,
Save, Save,
@ -43,6 +44,7 @@ import {
Bell, Bell,
Trash2, Trash2,
User, User,
Tags,
} from 'lucide-react' } from 'lucide-react'
export default function ProfileSettingsPage() { export default function ProfileSettingsPage() {
@ -58,6 +60,7 @@ export default function ProfileSettingsPage() {
const [bio, setBio] = useState('') const [bio, setBio] = useState('')
const [phoneNumber, setPhoneNumber] = useState('') const [phoneNumber, setPhoneNumber] = useState('')
const [notificationPreference, setNotificationPreference] = useState('EMAIL') const [notificationPreference, setNotificationPreference] = useState('EMAIL')
const [expertiseTags, setExpertiseTags] = useState<string[]>([])
const [profileLoaded, setProfileLoaded] = useState(false) const [profileLoaded, setProfileLoaded] = useState(false)
// Password form state // Password form state
@ -76,6 +79,7 @@ export default function ProfileSettingsPage() {
setBio((meta.bio as string) || '') setBio((meta.bio as string) || '')
setPhoneNumber(user.phoneNumber || '') setPhoneNumber(user.phoneNumber || '')
setNotificationPreference(user.notificationPreference || 'EMAIL') setNotificationPreference(user.notificationPreference || 'EMAIL')
setExpertiseTags(user.expertiseTags || [])
setProfileLoaded(true) setProfileLoaded(true)
} }
@ -86,6 +90,7 @@ export default function ProfileSettingsPage() {
bio, bio,
phoneNumber: phoneNumber || null, phoneNumber: phoneNumber || null,
notificationPreference: notificationPreference as 'EMAIL' | 'WHATSAPP' | 'BOTH' | 'NONE', notificationPreference: notificationPreference as 'EMAIL' | 'WHATSAPP' | 'BOTH' | 'NONE',
expertiseTags,
}) })
toast.success('Profile updated successfully') toast.success('Profile updated successfully')
refetch() refetch()
@ -294,6 +299,41 @@ export default function ProfileSettingsPage() {
</CardContent> </CardContent>
</Card> </Card>
{/* Expertise Tags */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Tags className="h-5 w-5" />
Expertise Tags
</CardTitle>
<CardDescription>
Select your areas of expertise to help with project matching
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<TagInput
value={expertiseTags}
onChange={setExpertiseTags}
placeholder="Select your expertise areas..."
maxTags={15}
/>
<div className="flex justify-end">
<Button
onClick={handleSaveProfile}
disabled={updateProfile.isPending}
>
{updateProfile.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<Save className="mr-2 h-4 w-4" />
)}
Save Expertise
</Button>
</div>
</CardContent>
</Card>
{/* Change Password */} {/* Change Password */}
<Card> <Card>
<CardHeader> <CardHeader>

View File

@ -78,10 +78,11 @@ export const userRouter = router({
bio: z.string().max(1000).optional(), bio: z.string().max(1000).optional(),
phoneNumber: z.string().max(20).optional().nullable(), phoneNumber: z.string().max(20).optional().nullable(),
notificationPreference: z.enum(['EMAIL', 'WHATSAPP', 'BOTH', 'NONE']).optional(), notificationPreference: z.enum(['EMAIL', 'WHATSAPP', 'BOTH', 'NONE']).optional(),
expertiseTags: z.array(z.string()).max(15).optional(),
}) })
) )
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const { bio, ...directFields } = input const { bio, expertiseTags, ...directFields } = input
// If bio is provided, merge it into metadataJson // If bio is provided, merge it into metadataJson
let metadataJson: Prisma.InputJsonValue | undefined let metadataJson: Prisma.InputJsonValue | undefined
@ -99,6 +100,7 @@ export const userRouter = router({
data: { data: {
...directFields, ...directFields,
...(metadataJson !== undefined && { metadataJson }), ...(metadataJson !== undefined && { metadataJson }),
...(expertiseTags !== undefined && { expertiseTags }),
}, },
}) })
}), }),
@ -241,14 +243,22 @@ export const userRouter = router({
get: adminProcedure get: adminProcedure
.input(z.object({ id: z.string() })) .input(z.object({ id: z.string() }))
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
return ctx.prisma.user.findUniqueOrThrow({ console.log('[user.get] Fetching user:', input.id)
where: { id: input.id }, try {
include: { const user = await ctx.prisma.user.findUniqueOrThrow({
_count: { where: { id: input.id },
select: { assignments: true, mentorAssignments: true }, include: {
_count: {
select: { assignments: true, mentorAssignments: true },
},
}, },
}, })
}) console.log('[user.get] Found user:', user.email)
return user
} catch (error) {
console.error('[user.get] Error fetching user:', input.id, error)
throw error
}
}), }),
/** /**