fix(admin): support typed question responses

Render database-backed text, audio, photo, and choice responses across admin activity and user detail views. Add support for the both_answered match state while retaining legacy swipe behavior.
This commit is contained in:
jjbrunton committed 2026-08-27 21:37:34 +01:00
1 parent 01bb8225ae
commit 6df9442cd0
6 files changed
+175 -42

No files matched your search

@@ -0,0 +1,32 @@
import type { AnswerType, QuestionType } from '@sauci/shared';
import { Badge } from '@/components/ui/badge';
import { formatAdminResponse, QUESTION_TYPE_LABELS, type AdminResponseData } from '@/lib/questionResponses';
interface ResponseAnswerProps {
answer: AnswerType;
questionType?: QuestionType | null;
responseData?: AdminResponseData;
}
export function ResponseAnswer({ answer, questionType, responseData = null }: ResponseAnswerProps) {
const formatted = formatAdminResponse(questionType, answer, responseData);
const resolvedType = questionType ?? 'swipe';
return (
<div className="min-w-0 space-y-1">
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm font-medium">{formatted.label}</span>
{resolvedType !== 'swipe' && (
<Badge variant="outline" className="text-[10px] font-normal">
{QUESTION_TYPE_LABELS[resolvedType]}
</Badge>
)}
</div>
{formatted.detail && (
<p className="max-w-md whitespace-pre-wrap break-words text-xs text-muted-foreground">
{formatted.detail}
</p>
)}
</div>
);
}
@@ -0,0 +1,27 @@
import { describe, expect, it } from 'vitest';
import { formatAdminResponse, MATCH_TYPE_LABELS, resolveQuestionType } from './questionResponses';
describe('admin question response formatting', () => {
it('keeps legacy questions compatible with swipe answers', () => {
expect(resolveQuestionType(null)).toBe('swipe');
expect(formatAdminResponse(null, 'maybe', null)).toEqual({ label: 'Maybe', detail: null });
});
it('shows typed response details without exposing media storage paths', () => {
expect(formatAdminResponse('text_answer', 'yes', { type: 'text_answer', text: ' My answer ' }))
.toEqual({ label: 'Text submitted', detail: 'My answer' });
expect(formatAdminResponse('audio', 'yes', {
type: 'audio',
media_path: 'private/user/audio.m4a',
duration_seconds: 12.6,
})).toEqual({ label: 'Audio submitted', detail: '13s recording' });
expect(formatAdminResponse('photo', 'yes', {
type: 'photo',
media_path: 'private/user/photo.jpg',
})).toEqual({ label: 'Photo submitted', detail: null });
});
it('labels non-swipe matches', () => {
expect(MATCH_TYPE_LABELS.both_answered).toBe('Both Answered');
});
});
+67
View File
@@ -0,0 +1,67 @@
import type { AnswerType, QuestionType } from '@sauci/shared';
export type AdminResponseData =
| { type: 'text_answer'; text: string }
| { type: 'audio'; media_path: string; duration_seconds: number }
| { type: 'photo'; media_path: string }
| { type: 'who_likely'; chosen_user_id: string }
| Record<string, unknown>
| null;
export const QUESTION_TYPE_LABELS: Record<QuestionType, string> = {
swipe: 'Swipe',
text_answer: 'Text answer',
audio: 'Audio',
photo: 'Photo',
who_likely: 'Who is more likely',
};
export const MATCH_TYPE_LABELS = {
yes_yes: 'Both Yes!',
yes_maybe: 'Yes + Maybe',
maybe_maybe: 'Both Maybe',
both_answered: 'Both Answered',
} as const;
export function resolveQuestionType(questionType?: QuestionType | null): QuestionType {
return questionType ?? 'swipe';
}
export function formatAdminResponse(
questionType: QuestionType | null | undefined,
answer: AnswerType,
responseData: AdminResponseData,
): { label: string; detail: string | null } {
const resolvedType = resolveQuestionType(questionType);
if (resolvedType === 'swipe') {
return {
label: answer.charAt(0).toUpperCase() + answer.slice(1),
detail: null,
};
}
if (resolvedType === 'text_answer') {
const text = responseData && 'text' in responseData && typeof responseData.text === 'string'
? responseData.text.trim()
: '';
return { label: 'Text submitted', detail: text || null };
}
if (resolvedType === 'audio') {
const duration = responseData && 'duration_seconds' in responseData
&& typeof responseData.duration_seconds === 'number'
? Math.max(0, Math.round(responseData.duration_seconds))
: null;
return {
label: 'Audio submitted',
detail: duration === null ? null : `${duration}s recording`,
};
}
if (resolvedType === 'photo') {
return { label: 'Photo submitted', detail: null };
}
return { label: 'Choice submitted', detail: null };
}
@@ -23,11 +23,12 @@ import {
} from '@/components/ui/tabs'; } from '@/components/ui/tabs';
import { formatDistanceToNow, format } from 'date-fns'; import { formatDistanceToNow, format } from 'date-fns';
import { toast } from 'sonner'; import { toast } from 'sonner';
import type { AnswerType, QuestionType } from '@sauci/shared';
import { ResponseAnswer } from '@/components/content/ResponseAnswer';
import { MATCH_TYPE_LABELS, type AdminResponseData } from '@/lib/questionResponses';
import { import {
Activity, Activity,
ThumbsUp, ThumbsUp,
ThumbsDown,
Minus,
Heart, Heart,
MessageCircle, MessageCircle,
UserPlus, UserPlus,
@@ -38,7 +39,8 @@ import {
interface ResponseActivity { interface ResponseActivity {
id: string; id: string;
user_id: string; user_id: string;
answer: 'yes' | 'no' | 'maybe'; answer: AnswerType;
response_data: AdminResponseData;
created_at: string; created_at: string;
profile: { profile: {
id: string; id: string;
@@ -48,6 +50,7 @@ interface ResponseActivity {
question: { question: {
id: string; id: string;
text: string; text: string;
question_type: QuestionType;
pack: { pack: {
id: string; id: string;
name: string; name: string;
@@ -58,7 +61,7 @@ interface ResponseActivity {
interface MatchActivity { interface MatchActivity {
id: string; id: string;
couple_id: string; couple_id: string;
match_type: 'yes_yes' | 'yes_maybe' | 'maybe_maybe'; match_type: keyof typeof MATCH_TYPE_LABELS;
is_new: boolean; is_new: boolean;
created_at: string; created_at: string;
question: { question: {
@@ -104,17 +107,11 @@ interface SignupActivity {
type ActivityTab = 'responses' | 'matches' | 'messages' | 'signups'; type ActivityTab = 'responses' | 'matches' | 'messages' | 'signups';
// Answer icons
const answerConfig = {
yes: { icon: <ThumbsUp className="h-4 w-4" />, color: 'text-green-500', bg: 'bg-green-500/10' },
no: { icon: <ThumbsDown className="h-4 w-4" />, color: 'text-red-500', bg: 'bg-red-500/10' },
maybe: { icon: <Minus className="h-4 w-4" />, color: 'text-yellow-500', bg: 'bg-yellow-500/10' },
};
const matchTypeLabels = { const matchTypeLabels = {
yes_yes: { label: 'Both Yes!', variant: 'default' as const }, yes_yes: { label: 'Both Yes!', variant: 'default' as const },
yes_maybe: { label: 'Yes + Maybe', variant: 'secondary' as const }, yes_maybe: { label: 'Yes + Maybe', variant: 'secondary' as const },
maybe_maybe: { label: 'Both Maybe', variant: 'outline' as const }, maybe_maybe: { label: 'Both Maybe', variant: 'outline' as const },
both_answered: { label: MATCH_TYPE_LABELS.both_answered, variant: 'secondary' as const },
}; };
export function UserActivityPage() { export function UserActivityPage() {
@@ -152,10 +149,10 @@ export function UserActivityPage() {
const { data, error, count } = await supabase const { data, error, count } = await supabase
.from('responses') .from('responses')
.select(` .select(`
id, user_id, answer, created_at, id, user_id, answer, response_data, created_at,
profile:profiles!responses_user_id_fkey(id, name, email), profile:profiles!responses_user_id_fkey(id, name, email),
question:questions( question:questions(
id, text, id, text, question_type,
pack:question_packs(id, name) pack:question_packs(id, name)
) )
`, { count: 'exact' }) `, { count: 'exact' })
@@ -171,11 +168,13 @@ export function UserActivityPage() {
id: item.id, id: item.id,
user_id: item.user_id, user_id: item.user_id,
answer: item.answer, answer: item.answer,
response_data: item.response_data,
created_at: item.created_at, created_at: item.created_at,
profile: item.profile, profile: item.profile,
question: item.question ? { question: item.question ? {
id: item.question.id, id: item.question.id,
text: item.question.text, text: item.question.text,
question_type: item.question.question_type ?? 'swipe',
pack: item.question.pack, pack: item.question.pack,
} : null, } : null,
})); }));
@@ -426,17 +425,6 @@ export function UserActivityPage() {
); );
}; };
// Render answer badge
const renderAnswerBadge = (answer: 'yes' | 'no' | 'maybe') => {
const config = answerConfig[answer];
return (
<span className={`inline-flex items-center gap-1 px-2 py-1 rounded-md ${config.bg} ${config.color}`}>
{config.icon}
<span className="text-sm font-medium capitalize">{answer}</span>
</span>
);
};
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Header */} {/* Header */}
@@ -533,7 +521,13 @@ export function UserActivityPage() {
</Badge> </Badge>
)} )}
</TableCell> </TableCell>
<TableCell>{renderAnswerBadge(item.answer)}</TableCell> <TableCell>
<ResponseAnswer
answer={item.answer}
questionType={item.question?.question_type}
responseData={item.response_data}
/>
</TableCell>
<TableCell className="text-muted-foreground text-sm whitespace-nowrap"> <TableCell className="text-muted-foreground text-sm whitespace-nowrap">
{formatDistanceToNow(new Date(item.created_at), { addSuffix: true })} {formatDistanceToNow(new Date(item.created_at), { addSuffix: true })}
</TableCell> </TableCell>
+3 -1
View File
@@ -13,6 +13,7 @@ import { cn } from '@/lib/utils';
import { useRealtimeSubscription } from '@/hooks/useRealtimeSubscription'; import { useRealtimeSubscription } from '@/hooks/useRealtimeSubscription';
import { RealtimeStatusIndicator } from '@/components/RealtimeStatusIndicator'; import { RealtimeStatusIndicator } from '@/components/RealtimeStatusIndicator';
import { PaginationControls } from '@/components/ui/pagination'; import { PaginationControls } from '@/components/ui/pagination';
import { MATCH_TYPE_LABELS } from '@/lib/questionResponses';
interface Profile { interface Profile {
id: string; id: string;
@@ -22,7 +23,7 @@ interface Profile {
interface Match { interface Match {
id: string; id: string;
match_type: 'yes_yes' | 'yes_maybe' | 'maybe_maybe'; match_type: keyof typeof MATCH_TYPE_LABELS;
question: { question: {
id: string; id: string;
text: string; text: string;
@@ -48,6 +49,7 @@ const matchTypeLabels = {
yes_yes: 'Both Yes!', yes_yes: 'Both Yes!',
yes_maybe: 'Yes + Maybe', yes_maybe: 'Yes + Maybe',
maybe_maybe: 'Both Maybe', maybe_maybe: 'Both Maybe',
both_answered: MATCH_TYPE_LABELS.both_answered,
}; };
function formatMessageTime(date: Date): string { function formatMessageTime(date: Date): string {
+27 -16
View File
@@ -37,9 +37,12 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select'; } from '@/components/ui/select';
import { Crown, Users, MessageCircle, ChevronRight, ThumbsUp, ThumbsDown, Minus, Gift, Image, Video as VideoIcon, Target, User, Eye, EyeOff, CheckCircle, Package, Sparkles, Flame, Trophy, Calendar, AlertCircle, Clock, Pencil } from 'lucide-react'; import { Crown, Users, MessageCircle, ChevronRight, Gift, Image, Video as VideoIcon, Target, User, Eye, EyeOff, CheckCircle, Package, Sparkles, Flame, Trophy, Calendar, AlertCircle, Clock, Pencil } from 'lucide-react';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { IconPreview } from '@/components/ui/icon-picker'; import { IconPreview } from '@/components/ui/icon-picker';
import type { AnswerType, QuestionType } from '@sauci/shared';
import { ResponseAnswer } from '@/components/content/ResponseAnswer';
import { MATCH_TYPE_LABELS, type AdminResponseData } from '@/lib/questionResponses';
type UsageReason = 'improve_communication' | 'spice_up_intimacy' | 'deeper_connection' | 'have_fun' | 'strengthen_relationship'; type UsageReason = 'improve_communication' | 'spice_up_intimacy' | 'deeper_connection' | 'have_fun' | 'strengthen_relationship';
type Gender = 'male' | 'female' | 'non-binary' | 'prefer-not-to-say'; type Gender = 'male' | 'female' | 'non-binary' | 'prefer-not-to-say';
@@ -82,11 +85,13 @@ interface Partner {
interface Response { interface Response {
id: string; id: string;
answer: 'yes' | 'no' | 'maybe'; answer: AnswerType;
response_data: AdminResponseData;
created_at: string | null; created_at: string | null;
question: { question: {
id: string; id: string;
text: string; text: string;
question_type: QuestionType;
pack: { pack: {
id: string; id: string;
name: string; name: string;
@@ -96,7 +101,7 @@ interface Response {
interface Match { interface Match {
id: string; id: string;
match_type: 'yes_yes' | 'yes_maybe' | 'maybe_maybe'; match_type: keyof typeof MATCH_TYPE_LABELS;
is_new: boolean; is_new: boolean;
created_at: string | null; created_at: string | null;
question: { question: {
@@ -134,12 +139,14 @@ interface OutstandingQuestion {
id: string; id: string;
text: string; text: string;
intensity: number | null; intensity: number | null;
question_type: QuestionType;
pack: { pack: {
id: string; id: string;
name: string; name: string;
}; };
partner_response: { partner_response: {
answer: 'yes' | 'no' | 'maybe'; answer: AnswerType;
response_data: AdminResponseData;
created_at: string | null; created_at: string | null;
}; };
} }
@@ -155,16 +162,11 @@ interface CoupleStreak {
updated_at: string; updated_at: string;
} }
const answerIcons = {
yes: <ThumbsUp className="h-4 w-4 text-green-500" />,
no: <ThumbsDown className="h-4 w-4 text-red-500" />,
maybe: <Minus className="h-4 w-4 text-yellow-500" />,
};
const matchTypeLabels = { const matchTypeLabels = {
yes_yes: 'Both Yes!', yes_yes: 'Both Yes!',
yes_maybe: 'Yes + Maybe', yes_maybe: 'Yes + Maybe',
maybe_maybe: 'Both Maybe', maybe_maybe: 'Both Maybe',
both_answered: MATCH_TYPE_LABELS.both_answered,
}; };
@@ -536,7 +538,7 @@ export function UserDetailPage() {
const { data: responseData, count: responsesCount, error: responsesError } = await supabase const { data: responseData, count: responsesCount, error: responsesError } = await supabase
.from('responses') .from('responses')
.select('id, answer, created_at, question:questions(id, text, pack:question_packs(id, name))', { count: 'exact' }) .select('id, answer, response_data, created_at, question:questions(id, text, question_type, pack:question_packs(id, name))', { count: 'exact' })
.eq('user_id', userId) .eq('user_id', userId)
.order('created_at', { ascending: false }) .order('created_at', { ascending: false })
.range(responsesFrom, responsesTo); .range(responsesFrom, responsesTo);
@@ -646,6 +648,7 @@ export function UserDetailPage() {
.select(` .select(`
question_id, question_id,
answer, answer,
response_data,
created_at created_at
`) `)
.eq('user_id', partnerProfile.id) .eq('user_id', partnerProfile.id)
@@ -660,7 +663,7 @@ export function UserDetailPage() {
// Fetch question details (no pack filter — admin sees all) // Fetch question details (no pack filter — admin sees all)
const { data: questionDetails } = await supabase const { data: questionDetails } = await supabase
.from('questions') .from('questions')
.select('id, text, intensity, pack:question_packs(id, name)') .select('id, text, intensity, question_type, pack:question_packs(id, name)')
.in('id', unansweredQuestionIds); .in('id', unansweredQuestionIds);
const questionMap = new Map((questionDetails || []).map((q: any) => [q.id, q])); const questionMap = new Map((questionDetails || []).map((q: any) => [q.id, q]));
@@ -675,8 +678,13 @@ export function UserDetailPage() {
id: qId, id: qId,
text: q.text || '', text: q.text || '',
intensity: q.intensity || null, intensity: q.intensity || null,
question_type: q.question_type ?? 'swipe',
pack: { id: q.pack?.id || '', name: q.pack?.name || '' }, pack: { id: q.pack?.id || '', name: q.pack?.name || '' },
partner_response: { answer: pr.answer, created_at: pr.created_at }, partner_response: {
answer: pr.answer,
response_data: pr.response_data,
created_at: pr.created_at,
},
}); });
} }
} }
@@ -897,7 +905,7 @@ export function UserDetailPage() {
<TabsTrigger value="packs" disabled={!profile.couple_id}>Enabled Packs ({enabledPacks.length})</TabsTrigger> <TabsTrigger value="packs" disabled={!profile.couple_id}>Enabled Packs ({enabledPacks.length})</TabsTrigger>
<TabsTrigger value="livedraw" disabled={!profile.couple_id}>Live Draw</TabsTrigger> <TabsTrigger value="livedraw" disabled={!profile.couple_id}>Live Draw</TabsTrigger>
</TabsList> </TabsList>
{canViewResponses && <TabsContent value="responses" className="mt-4">{responses.length === 0 ? <Card className="flex flex-col items-center justify-center py-12"><p className="text-muted-foreground">No responses yet</p></Card> : <div className="space-y-4"><div className="rounded-md border"><Table><TableHeader><TableRow><TableHead>Question</TableHead><TableHead>Pack</TableHead><TableHead className="w-24">Answer</TableHead><TableHead className="w-32">Date</TableHead></TableRow></TableHeader><TableBody>{responses.map((r) => (<TableRow key={r.id}><TableCell className="max-w-md">{r.question.pack?.id ? (<Link to={`/packs/${r.question.pack.id}/questions`} className="line-clamp-2 text-primary hover:underline cursor-pointer">{r.question.text}</Link>) : (<span className="line-clamp-2">{r.question.text}</span>)}</TableCell><TableCell>{r.question.pack?.id ? (<Link to={`/packs/${r.question.pack.id}/questions`} className="text-primary hover:underline">{r.question.pack.name}</Link>) : (<span className="text-muted-foreground">{r.question.pack?.name || '—'}</span>)}</TableCell><TableCell><div className="flex items-center gap-2">{answerIcons[r.answer]}<span className="capitalize">{r.answer}</span></div></TableCell><TableCell className="text-muted-foreground text-sm">{r.created_at ? format(new Date(r.created_at), 'MMM d, yyyy') : '—'}</TableCell></TableRow>))}</TableBody></Table></div><PaginationControls page={responsesPage} pageSize={responsesPageSize} totalCount={responsesTotal} onPageChange={setResponsesPage} onPageSizeChange={(size) => { setResponsesPage(1); setResponsesPageSize(size); }} /></div>}</TabsContent>} {canViewResponses && <TabsContent value="responses" className="mt-4">{responses.length === 0 ? <Card className="flex flex-col items-center justify-center py-12"><p className="text-muted-foreground">No responses yet</p></Card> : <div className="space-y-4"><div className="rounded-md border"><Table><TableHeader><TableRow><TableHead>Question</TableHead><TableHead>Pack</TableHead><TableHead className="w-64">Response</TableHead><TableHead className="w-32">Date</TableHead></TableRow></TableHeader><TableBody>{responses.map((r) => (<TableRow key={r.id}><TableCell className="max-w-md">{r.question.pack?.id ? (<Link to={`/packs/${r.question.pack.id}/questions`} className="line-clamp-2 text-primary hover:underline cursor-pointer">{r.question.text}</Link>) : (<span className="line-clamp-2">{r.question.text}</span>)}</TableCell><TableCell>{r.question.pack?.id ? (<Link to={`/packs/${r.question.pack.id}/questions`} className="text-primary hover:underline">{r.question.pack.name}</Link>) : (<span className="text-muted-foreground">{r.question.pack?.name || '—'}</span>)}</TableCell><TableCell><ResponseAnswer answer={r.answer} questionType={r.question.question_type} responseData={r.response_data} /></TableCell><TableCell className="text-muted-foreground text-sm">{r.created_at ? format(new Date(r.created_at), 'MMM d, yyyy') : '—'}</TableCell></TableRow>))}</TableBody></Table></div><PaginationControls page={responsesPage} pageSize={responsesPageSize} totalCount={responsesTotal} onPageChange={setResponsesPage} onPageSizeChange={(size) => { setResponsesPage(1); setResponsesPageSize(size); }} /></div>}</TabsContent>}
{canViewMatches && <TabsContent value="matches" className="mt-4">{matches.length === 0 ? <Card className="flex flex-col items-center justify-center py-12"><p className="text-muted-foreground">No matches yet</p></Card> : <div className="space-y-4"><div className="grid gap-4 md:grid-cols-2">{matches.map((match) => (<Card key={match.id} className="hover:shadow-md transition-shadow"><CardHeader className="pb-2"><div className="flex items-start justify-between"><Badge variant="secondary">{matchTypeLabels[match.match_type]}</Badge>{match.is_new && <Badge variant="default" className="text-xs">New</Badge>}</div><CardDescription className="line-clamp-2 mt-2">{match.question?.text || 'Unknown question'}</CardDescription></CardHeader><CardContent><div className="flex items-center justify-between"><div className="flex items-center gap-1 text-sm text-muted-foreground"><MessageCircle className="h-4 w-4" />{match.message_count} message{match.message_count !== 1 ? 's' : ''}</div>{canViewChats && <Link to={`/users/${userId}/matches/${match.id}`}><Button variant="ghost" size="sm">View Chat<ChevronRight className="ml-1 h-4 w-4" /></Button></Link>}</div></CardContent></Card>))}</div><PaginationControls page={matchesPage} pageSize={matchesPageSize} totalCount={matchesTotal} onPageChange={setMatchesPage} onPageSizeChange={(size) => { setMatchesPage(1); setMatchesPageSize(size); }} /></div>}</TabsContent>} {canViewMatches && <TabsContent value="matches" className="mt-4">{matches.length === 0 ? <Card className="flex flex-col items-center justify-center py-12"><p className="text-muted-foreground">No matches yet</p></Card> : <div className="space-y-4"><div className="grid gap-4 md:grid-cols-2">{matches.map((match) => (<Card key={match.id} className="hover:shadow-md transition-shadow"><CardHeader className="pb-2"><div className="flex items-start justify-between"><Badge variant="secondary">{matchTypeLabels[match.match_type]}</Badge>{match.is_new && <Badge variant="default" className="text-xs">New</Badge>}</div><CardDescription className="line-clamp-2 mt-2">{match.question?.text || 'Unknown question'}</CardDescription></CardHeader><CardContent><div className="flex items-center justify-between"><div className="flex items-center gap-1 text-sm text-muted-foreground"><MessageCircle className="h-4 w-4" />{match.message_count} message{match.message_count !== 1 ? 's' : ''}</div>{canViewChats && <Link to={`/users/${userId}/matches/${match.id}`}><Button variant="ghost" size="sm">View Chat<ChevronRight className="ml-1 h-4 w-4" /></Button></Link>}</div></CardContent></Card>))}</div><PaginationControls page={matchesPage} pageSize={matchesPageSize} totalCount={matchesTotal} onPageChange={setMatchesPage} onPageSizeChange={(size) => { setMatchesPage(1); setMatchesPageSize(size); }} /></div>}</TabsContent>}
{canViewMedia && <TabsContent value="media" className="mt-4">{mediaMessages.length === 0 ? <Card className="flex flex-col items-center justify-center py-12"><Image className="h-12 w-12 text-muted-foreground mb-4" /><p className="text-muted-foreground">No media uploaded</p></Card> : <div className="space-y-4"><div className="grid gap-4 grid-cols-2 md:grid-cols-3 lg:grid-cols-4">{mediaMessages.map((message) => (<Card key={message.id} className="overflow-hidden"><div className="aspect-square bg-muted relative"> {canViewMedia && <TabsContent value="media" className="mt-4">{mediaMessages.length === 0 ? <Card className="flex flex-col items-center justify-center py-12"><Image className="h-12 w-12 text-muted-foreground mb-4" /><p className="text-muted-foreground">No media uploaded</p></Card> : <div className="space-y-4"><div className="grid gap-4 grid-cols-2 md:grid-cols-3 lg:grid-cols-4">{mediaMessages.map((message) => (<Card key={message.id} className="overflow-hidden"><div className="aspect-square bg-muted relative">
{message.media_type === 'video' ? ( {message.media_type === 'video' ? (
@@ -964,8 +972,11 @@ export function UserDetailPage() {
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{answerIcons[q.partner_response.answer]} <ResponseAnswer
<span className="capitalize">{q.partner_response.answer}</span> answer={q.partner_response.answer}
questionType={q.question_type}
responseData={q.partner_response.response_data}
/>
</div> </div>
</TableCell> </TableCell>
<TableCell className="text-muted-foreground text-sm"> <TableCell className="text-muted-foreground text-sm">