mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-11-01 18:19:43 +00:00
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:
1 parent
01bb8225ae
commit
6df9442cd0
6 files changed
+175
-42
No files matched your search
@@ -13,6 +13,7 @@ import { cn } from '@/lib/utils';
|
||||
import { useRealtimeSubscription } from '@/hooks/useRealtimeSubscription';
|
||||
import { RealtimeStatusIndicator } from '@/components/RealtimeStatusIndicator';
|
||||
import { PaginationControls } from '@/components/ui/pagination';
|
||||
import { MATCH_TYPE_LABELS } from '@/lib/questionResponses';
|
||||
|
||||
interface Profile {
|
||||
id: string;
|
||||
@@ -22,7 +23,7 @@ interface Profile {
|
||||
|
||||
interface Match {
|
||||
id: string;
|
||||
match_type: 'yes_yes' | 'yes_maybe' | 'maybe_maybe';
|
||||
match_type: keyof typeof MATCH_TYPE_LABELS;
|
||||
question: {
|
||||
id: string;
|
||||
text: string;
|
||||
@@ -48,6 +49,7 @@ const matchTypeLabels = {
|
||||
yes_yes: 'Both Yes!',
|
||||
yes_maybe: 'Yes + Maybe',
|
||||
maybe_maybe: 'Both Maybe',
|
||||
both_answered: MATCH_TYPE_LABELS.both_answered,
|
||||
};
|
||||
|
||||
function formatMessageTime(date: Date): string {
|
||||
|
||||
@@ -37,9 +37,12 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} 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 { 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 Gender = 'male' | 'female' | 'non-binary' | 'prefer-not-to-say';
|
||||
@@ -82,11 +85,13 @@ interface Partner {
|
||||
|
||||
interface Response {
|
||||
id: string;
|
||||
answer: 'yes' | 'no' | 'maybe';
|
||||
answer: AnswerType;
|
||||
response_data: AdminResponseData;
|
||||
created_at: string | null;
|
||||
question: {
|
||||
id: string;
|
||||
text: string;
|
||||
question_type: QuestionType;
|
||||
pack: {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -96,7 +101,7 @@ interface Response {
|
||||
|
||||
interface Match {
|
||||
id: string;
|
||||
match_type: 'yes_yes' | 'yes_maybe' | 'maybe_maybe';
|
||||
match_type: keyof typeof MATCH_TYPE_LABELS;
|
||||
is_new: boolean;
|
||||
created_at: string | null;
|
||||
question: {
|
||||
@@ -134,12 +139,14 @@ interface OutstandingQuestion {
|
||||
id: string;
|
||||
text: string;
|
||||
intensity: number | null;
|
||||
question_type: QuestionType;
|
||||
pack: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
partner_response: {
|
||||
answer: 'yes' | 'no' | 'maybe';
|
||||
answer: AnswerType;
|
||||
response_data: AdminResponseData;
|
||||
created_at: string | null;
|
||||
};
|
||||
}
|
||||
@@ -155,16 +162,11 @@ interface CoupleStreak {
|
||||
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 = {
|
||||
yes_yes: 'Both Yes!',
|
||||
yes_maybe: 'Yes + 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
|
||||
.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)
|
||||
.order('created_at', { ascending: false })
|
||||
.range(responsesFrom, responsesTo);
|
||||
@@ -646,6 +648,7 @@ export function UserDetailPage() {
|
||||
.select(`
|
||||
question_id,
|
||||
answer,
|
||||
response_data,
|
||||
created_at
|
||||
`)
|
||||
.eq('user_id', partnerProfile.id)
|
||||
@@ -660,7 +663,7 @@ export function UserDetailPage() {
|
||||
// Fetch question details (no pack filter — admin sees all)
|
||||
const { data: questionDetails } = await supabase
|
||||
.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);
|
||||
|
||||
const questionMap = new Map((questionDetails || []).map((q: any) => [q.id, q]));
|
||||
@@ -675,8 +678,13 @@ export function UserDetailPage() {
|
||||
id: qId,
|
||||
text: q.text || '',
|
||||
intensity: q.intensity || null,
|
||||
question_type: q.question_type ?? 'swipe',
|
||||
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="livedraw" disabled={!profile.couple_id}>Live Draw</TabsTrigger>
|
||||
</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>}
|
||||
{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' ? (
|
||||
@@ -964,8 +972,11 @@ export function UserDetailPage() {
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
{answerIcons[q.partner_response.answer]}
|
||||
<span className="capitalize">{q.partner_response.answer}</span>
|
||||
<ResponseAnswer
|
||||
answer={q.partner_response.answer}
|
||||
questionType={q.question_type}
|
||||
responseData={q.partner_response.response_data}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground text-sm">
|
||||
|
||||
Reference in new issue
Block a user