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 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 sendInvitation = trpc.user.sendInvitation.useMutation()
@ -121,14 +121,19 @@ export default function MemberDetailPage() {
)
}
if (!user) {
if (error || !user) {
return (
<div className="space-y-6">
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Member not found</AlertTitle>
<AlertTitle>Error Loading Member</AlertTitle>
<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>
</Alert>
<Button asChild>

View File

@ -35,6 +35,7 @@ import {
import { Skeleton } from '@/components/ui/skeleton'
import { AvatarUpload } from '@/components/shared/avatar-upload'
import { UserAvatar } from '@/components/shared/user-avatar'
import { TagInput } from '@/components/shared/tag-input'
import {
Loader2,
Save,
@ -43,6 +44,7 @@ import {
Bell,
Trash2,
User,
Tags,
} from 'lucide-react'
export default function ProfileSettingsPage() {
@ -58,6 +60,7 @@ export default function ProfileSettingsPage() {
const [bio, setBio] = useState('')
const [phoneNumber, setPhoneNumber] = useState('')
const [notificationPreference, setNotificationPreference] = useState('EMAIL')
const [expertiseTags, setExpertiseTags] = useState<string[]>([])
const [profileLoaded, setProfileLoaded] = useState(false)
// Password form state
@ -76,6 +79,7 @@ export default function ProfileSettingsPage() {
setBio((meta.bio as string) || '')
setPhoneNumber(user.phoneNumber || '')
setNotificationPreference(user.notificationPreference || 'EMAIL')
setExpertiseTags(user.expertiseTags || [])
setProfileLoaded(true)
}
@ -86,6 +90,7 @@ export default function ProfileSettingsPage() {
bio,
phoneNumber: phoneNumber || null,
notificationPreference: notificationPreference as 'EMAIL' | 'WHATSAPP' | 'BOTH' | 'NONE',
expertiseTags,
})
toast.success('Profile updated successfully')
refetch()
@ -294,6 +299,41 @@ export default function ProfileSettingsPage() {
</CardContent>
</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 */}
<Card>
<CardHeader>

View File

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