mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
695 lines
33 KiB
TypeScript
695 lines
33 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react';
|
|
import { useParams } from 'react-router-dom';
|
|
import { supabase } from '@/config';
|
|
import { auditedSupabase } from '@/hooks/useAuditedSupabase';
|
|
import { useRealtimeSubscription } from '@/hooks/useRealtimeSubscription';
|
|
import { RealtimeStatusIndicator } from '@/components/RealtimeStatusIndicator';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@/components/ui/table';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from '@/components/ui/dialog';
|
|
import { Card } from '@/components/ui/card';
|
|
import { Plus, Pencil, Trash2, Sparkles, Loader2, Users } from 'lucide-react';
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
import { toast } from 'sonner';
|
|
import { AiGeneratorDialog } from '@/components/ai/AiGeneratorDialog';
|
|
import { AIPolishButton } from '@/components/ai/AIPolishButton';
|
|
import { ReviewQuestionsDialog } from '@/components/content/ReviewQuestionsDialog';
|
|
|
|
interface QuestionPack {
|
|
id: string;
|
|
name: string;
|
|
description: string | null;
|
|
icon: string | null;
|
|
is_explicit: boolean;
|
|
}
|
|
|
|
interface Question {
|
|
id: string;
|
|
pack_id: string;
|
|
text: string;
|
|
partner_text: string | null;
|
|
intensity: number;
|
|
allowed_couple_genders: string[] | null;
|
|
target_user_genders: string[] | null;
|
|
created_at: string | null;
|
|
}
|
|
|
|
import { INTENSITY_LEVELS } from '@/lib/openai';
|
|
|
|
// Helper to get intensity info by level (1-indexed)
|
|
const getIntensity = (level: number) => INTENSITY_LEVELS[level - 1] || INTENSITY_LEVELS[0];
|
|
|
|
export function QuestionsPage() {
|
|
const { packId } = useParams<{ packId: string }>();
|
|
const [pack, setPack] = useState<QuestionPack | null>(null);
|
|
const [questions, setQuestions] = useState<Question[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
const [aiDialogOpen, setAiDialogOpen] = useState(false);
|
|
const [reviewOpen, setReviewOpen] = useState(false);
|
|
const [editingQuestion, setEditingQuestion] = useState<Question | null>(null);
|
|
const [saving, setSaving] = useState(false);
|
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
|
const [bulkDeleting, setBulkDeleting] = useState(false);
|
|
|
|
// Form state
|
|
const [formData, setFormData] = useState<{
|
|
text: string;
|
|
partner_text: string;
|
|
intensity: number;
|
|
allowed_couple_genders: string[];
|
|
target_user_genders: string[];
|
|
}>({
|
|
text: '',
|
|
partner_text: '',
|
|
intensity: 1,
|
|
allowed_couple_genders: [],
|
|
target_user_genders: [],
|
|
});
|
|
|
|
const fetchData = useCallback(async () => {
|
|
if (!packId) return;
|
|
|
|
setLoading(true);
|
|
try {
|
|
// Fetch pack info
|
|
const { data: packData } = await supabase
|
|
.from('question_packs')
|
|
.select('id, name, description, icon, is_explicit')
|
|
.eq('id', packId)
|
|
.single();
|
|
|
|
setPack(packData);
|
|
|
|
// Fetch questions
|
|
const { data: questionData, error } = await supabase
|
|
.from('questions')
|
|
.select('*')
|
|
.eq('pack_id', packId)
|
|
.order('created_at', { ascending: true });
|
|
|
|
if (error) throw error;
|
|
setQuestions(questionData || []);
|
|
} catch (error) {
|
|
toast.error('Failed to load questions');
|
|
console.error(error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [packId]);
|
|
|
|
// Real-time subscription for questions in this pack
|
|
const { status: realtimeStatus } = useRealtimeSubscription<Question>({
|
|
table: 'questions',
|
|
filter: packId ? `pack_id=eq.${packId}` : undefined,
|
|
enabled: !!packId,
|
|
onInsert: useCallback((newQuestion: Question) => {
|
|
setQuestions(prev => [...prev, newQuestion]);
|
|
}, []),
|
|
onUpdate: useCallback(({ new: updated }: { old: Question; new: Question }) => {
|
|
setQuestions(prev => prev.map(q => q.id === updated.id ? updated : q));
|
|
}, []),
|
|
onDelete: useCallback((deleted: Question) => {
|
|
setQuestions(prev => prev.filter(q => q.id !== deleted.id));
|
|
setSelectedIds(prev => {
|
|
const next = new Set(prev);
|
|
next.delete(deleted.id);
|
|
return next;
|
|
});
|
|
}, []),
|
|
});
|
|
|
|
useEffect(() => {
|
|
fetchData();
|
|
}, [fetchData]);
|
|
|
|
const openCreateDialog = () => {
|
|
setEditingQuestion(null);
|
|
setFormData({ text: '', partner_text: '', intensity: 1, allowed_couple_genders: [], target_user_genders: [] });
|
|
setDialogOpen(true);
|
|
};
|
|
|
|
const openEditDialog = (question: Question) => {
|
|
setEditingQuestion(question);
|
|
setFormData({
|
|
text: question.text,
|
|
partner_text: question.partner_text || '',
|
|
intensity: question.intensity,
|
|
allowed_couple_genders: question.allowed_couple_genders || [],
|
|
target_user_genders: question.target_user_genders || [],
|
|
});
|
|
setDialogOpen(true);
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
if (!formData.text.trim()) {
|
|
toast.error('Question text is required');
|
|
return;
|
|
}
|
|
|
|
setSaving(true);
|
|
try {
|
|
if (editingQuestion) {
|
|
const { error } = await auditedSupabase.update('questions', editingQuestion.id, {
|
|
text: formData.text,
|
|
partner_text: formData.partner_text || null,
|
|
intensity: formData.intensity,
|
|
allowed_couple_genders: formData.allowed_couple_genders.length > 0 ? formData.allowed_couple_genders : null,
|
|
target_user_genders: formData.target_user_genders.length > 0 ? formData.target_user_genders : null,
|
|
});
|
|
|
|
if (error) throw error;
|
|
toast.success('Question updated');
|
|
} else {
|
|
const { error } = await auditedSupabase.insert('questions', {
|
|
pack_id: packId,
|
|
text: formData.text,
|
|
partner_text: formData.partner_text || null,
|
|
intensity: formData.intensity,
|
|
allowed_couple_genders: formData.allowed_couple_genders.length > 0 ? formData.allowed_couple_genders : null,
|
|
target_user_genders: formData.target_user_genders.length > 0 ? formData.target_user_genders : null,
|
|
});
|
|
|
|
if (error) throw error;
|
|
toast.success('Question created');
|
|
}
|
|
|
|
setDialogOpen(false);
|
|
fetchData();
|
|
} catch (error) {
|
|
toast.error('Failed to save question');
|
|
console.error(error);
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (question: Question) => {
|
|
if (!confirm('Delete this question? This cannot be undone.')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const { error } = await auditedSupabase.delete('questions', question.id);
|
|
|
|
if (error) throw error;
|
|
toast.success('Question deleted');
|
|
fetchData();
|
|
} catch (error) {
|
|
toast.error('Failed to delete question');
|
|
console.error(error);
|
|
}
|
|
};
|
|
|
|
const handleAiGenerated = (generatedQuestions: Array<{ text: string; partner_text?: string; intensity: number }>) => {
|
|
// Bulk insert AI-generated questions
|
|
const insertQuestions = async () => {
|
|
try {
|
|
const { error } = await auditedSupabase.insert(
|
|
'questions',
|
|
generatedQuestions.map(q => ({
|
|
pack_id: packId,
|
|
text: q.text,
|
|
partner_text: q.partner_text || null,
|
|
intensity: q.intensity,
|
|
}))
|
|
);
|
|
|
|
if (error) throw error;
|
|
toast.success(`${generatedQuestions.length} questions added`);
|
|
fetchData();
|
|
} catch (error) {
|
|
toast.error('Failed to add questions');
|
|
console.error(error);
|
|
}
|
|
};
|
|
|
|
insertQuestions();
|
|
setAiDialogOpen(false);
|
|
};
|
|
|
|
// Selection handlers
|
|
const toggleSelection = (id: string) => {
|
|
const next = new Set(selectedIds);
|
|
if (next.has(id)) {
|
|
next.delete(id);
|
|
} else {
|
|
next.add(id);
|
|
}
|
|
setSelectedIds(next);
|
|
};
|
|
|
|
const toggleSelectAll = () => {
|
|
if (selectedIds.size === questions.length) {
|
|
setSelectedIds(new Set());
|
|
} else {
|
|
setSelectedIds(new Set(questions.map(q => q.id)));
|
|
}
|
|
};
|
|
|
|
const handleBulkDelete = async () => {
|
|
if (selectedIds.size === 0) return;
|
|
|
|
const count = selectedIds.size;
|
|
if (!confirm(`Delete ${count} question${count !== 1 ? 's' : ''}? This cannot be undone.`)) {
|
|
return;
|
|
}
|
|
|
|
setBulkDeleting(true);
|
|
try {
|
|
await Promise.all(
|
|
Array.from(selectedIds).map(id => auditedSupabase.delete('questions', id))
|
|
);
|
|
toast.success(`${count} question${count !== 1 ? 's' : ''} deleted`);
|
|
setSelectedIds(new Set());
|
|
fetchData();
|
|
} catch (error) {
|
|
toast.error('Failed to delete questions');
|
|
console.error(error);
|
|
} finally {
|
|
setBulkDeleting(false);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<Skeleton className="h-9 w-64" />
|
|
<Skeleton className="h-10 w-40" />
|
|
</div>
|
|
<Skeleton className="h-96" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<div className="flex items-center gap-3">
|
|
<h1 className="text-3xl font-bold tracking-tight flex items-center gap-2">
|
|
{pack && (
|
|
<>
|
|
<span className="text-3xl">{pack.icon}</span>
|
|
{pack.name}
|
|
</>
|
|
)}
|
|
</h1>
|
|
<RealtimeStatusIndicator status={realtimeStatus} showLabel />
|
|
</div>
|
|
<p className="text-muted-foreground">
|
|
{questions.length} question{questions.length !== 1 ? 's' : ''} in this pack
|
|
</p>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button variant="outline" onClick={() => setReviewOpen(true)}>
|
|
<Sparkles className="mr-2 h-4 w-4" />
|
|
Review Questions
|
|
</Button>
|
|
<Button variant="outline" onClick={() => setAiDialogOpen(true)}>
|
|
<Sparkles className="mr-2 h-4 w-4" />
|
|
AI Generate
|
|
</Button>
|
|
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button onClick={openCreateDialog}>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
Add Question
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent className="max-w-2xl">
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
{editingQuestion ? 'Edit Question' : 'Add Question'}
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
{editingQuestion
|
|
? 'Update the question details below.'
|
|
: 'Add a new question to this pack.'}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4 py-4">
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<Label htmlFor="text">Question Text</Label>
|
|
<AIPolishButton
|
|
text={formData.text}
|
|
type="question"
|
|
explicit={pack?.is_explicit ?? false}
|
|
onPolished={(val) => setFormData(d => ({ ...d, text: val }))}
|
|
/>
|
|
</div>
|
|
<Textarea
|
|
id="text"
|
|
value={formData.text}
|
|
onChange={(e) => setFormData(d => ({ ...d, text: e.target.value }))}
|
|
placeholder="e.g., Would you try..."
|
|
rows={3}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<Label htmlFor="partner_text">Partner Text (optional)</Label>
|
|
<Badge variant="secondary" className="text-xs">
|
|
<Users className="h-3 w-3 mr-1" />
|
|
Two-Part
|
|
</Badge>
|
|
</div>
|
|
<AIPolishButton
|
|
text={formData.partner_text}
|
|
type="partner_text"
|
|
explicit={pack?.is_explicit ?? false}
|
|
onPolished={(val) => setFormData(d => ({ ...d, partner_text: val }))}
|
|
/>
|
|
</div>
|
|
<Textarea
|
|
id="partner_text"
|
|
value={formData.partner_text}
|
|
onChange={(e) => setFormData(d => ({ ...d, partner_text: e.target.value }))}
|
|
placeholder="Alternative text shown to the partner (for role-specific questions)"
|
|
rows={2}
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
If set, the partner will see this text instead. Useful for "Would you..." / "Would your partner..." style questions.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label>Intensity Level</Label>
|
|
<div className="flex gap-2">
|
|
{INTENSITY_LEVELS.map((intensity) => (
|
|
<Button
|
|
key={intensity.level}
|
|
type="button"
|
|
variant={formData.intensity === intensity.level ? 'default' : 'outline'}
|
|
size="sm"
|
|
onClick={() => setFormData(d => ({ ...d, intensity: intensity.level }))}
|
|
className={formData.intensity === intensity.level ? intensity.color : ''}
|
|
title={intensity.description}
|
|
>
|
|
{intensity.level} - {intensity.label}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div className="space-y-2">
|
|
<Label>Target Couples (optional)</Label>
|
|
<div className="flex flex-wrap gap-2">
|
|
{[
|
|
{ id: 'male+male', label: 'Male + Male' },
|
|
{ id: 'female+male', label: 'Male + Female' },
|
|
{ id: 'female+female', label: 'Female + Female' },
|
|
].map((type) => {
|
|
const isSelected = formData.allowed_couple_genders.includes(type.id);
|
|
return (
|
|
<Button
|
|
key={type.id}
|
|
type="button"
|
|
variant={isSelected ? 'default' : 'outline'}
|
|
size="sm"
|
|
onClick={() => {
|
|
setFormData(d => ({
|
|
...d,
|
|
allowed_couple_genders: isSelected
|
|
? d.allowed_couple_genders.filter(id => id !== type.id)
|
|
: [...d.allowed_couple_genders, type.id]
|
|
}));
|
|
}}
|
|
>
|
|
{type.label}
|
|
</Button>
|
|
);
|
|
})}
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
Select specific couple types to restrict this question. Leave all unselected to allow for everyone.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label>Initiator Gender (optional)</Label>
|
|
<div className="flex flex-wrap gap-2">
|
|
{[
|
|
{ id: 'male', label: 'Male' },
|
|
{ id: 'female', label: 'Female' },
|
|
{ id: 'non-binary', label: 'Non-binary' },
|
|
].map((type) => {
|
|
const isSelected = formData.target_user_genders.includes(type.id);
|
|
return (
|
|
<Button
|
|
key={type.id}
|
|
type="button"
|
|
variant={isSelected ? 'default' : 'outline'}
|
|
size="sm"
|
|
onClick={() => {
|
|
setFormData(d => ({
|
|
...d,
|
|
target_user_genders: isSelected
|
|
? d.target_user_genders.filter(id => id !== type.id)
|
|
: [...d.target_user_genders, type.id]
|
|
}));
|
|
}}
|
|
>
|
|
{type.label}
|
|
</Button>
|
|
);
|
|
})}
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
Who sees this question first? Their partner will see the Partner Text after they answer. Leave empty for anyone.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setDialogOpen(false)}>
|
|
Cancel
|
|
</Button>
|
|
<Button onClick={handleSave} disabled={saving}>
|
|
{saving ? (
|
|
<>
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
Saving...
|
|
</>
|
|
) : (
|
|
'Save'
|
|
)}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Bulk Action Bar */}
|
|
{selectedIds.size > 0 && (
|
|
<div className="flex items-center justify-between bg-muted/50 border rounded-lg px-4 py-3">
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-sm font-medium">
|
|
{selectedIds.size} question{selectedIds.size !== 1 ? 's' : ''} selected
|
|
</span>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setSelectedIds(new Set())}
|
|
>
|
|
Clear selection
|
|
</Button>
|
|
</div>
|
|
<Button
|
|
variant="destructive"
|
|
size="sm"
|
|
onClick={handleBulkDelete}
|
|
disabled={bulkDeleting}
|
|
>
|
|
{bulkDeleting ? (
|
|
<>
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
Deleting...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Trash2 className="mr-2 h-4 w-4" />
|
|
Delete {selectedIds.size} Question{selectedIds.size !== 1 ? 's' : ''}
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
{/* AI Generator Dialog */}
|
|
<AiGeneratorDialog
|
|
open={aiDialogOpen}
|
|
onOpenChange={setAiDialogOpen}
|
|
type="questions"
|
|
context={{ packName: pack?.name, packDescription: pack?.description, isExplicit: pack?.is_explicit, existingQuestions: questions.map(q => q.text) }}
|
|
onGenerated={handleAiGenerated}
|
|
/>
|
|
|
|
<ReviewQuestionsDialog
|
|
open={reviewOpen}
|
|
onOpenChange={setReviewOpen}
|
|
questions={questions}
|
|
isExplicit={pack?.is_explicit ?? false}
|
|
onUpdated={fetchData}
|
|
/>
|
|
|
|
{/* Questions Table */}
|
|
{
|
|
questions.length === 0 ? (
|
|
<Card className="flex flex-col items-center justify-center py-12">
|
|
<Sparkles className="h-12 w-12 text-muted-foreground mb-4" />
|
|
<h3 className="text-lg font-semibold">No questions yet</h3>
|
|
<p className="text-muted-foreground mb-4">
|
|
Add questions manually or generate them with AI
|
|
</p>
|
|
<div className="flex gap-2">
|
|
<Button variant="outline" onClick={() => setAiDialogOpen(true)}>
|
|
<Sparkles className="mr-2 h-4 w-4" />
|
|
AI Generate
|
|
</Button>
|
|
<Button onClick={openCreateDialog}>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
Add Question
|
|
</Button>
|
|
</div>
|
|
</Card>
|
|
) : (
|
|
<div className="rounded-md border">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="w-12">
|
|
<Checkbox
|
|
checked={selectedIds.size === questions.length && questions.length > 0}
|
|
onCheckedChange={toggleSelectAll}
|
|
aria-label="Select all"
|
|
/>
|
|
</TableHead>
|
|
<TableHead className="w-12">#</TableHead>
|
|
<TableHead>Question</TableHead>
|
|
<TableHead className="w-32">Partner Text</TableHead>
|
|
<TableHead className="w-24">Couples</TableHead>
|
|
<TableHead className="w-24">Initiator</TableHead>
|
|
<TableHead className="w-24">Intensity</TableHead>
|
|
<TableHead className="w-24">Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{questions.map((question, index) => (
|
|
<TableRow
|
|
key={question.id}
|
|
className={selectedIds.has(question.id) ? 'bg-muted/50' : ''}
|
|
>
|
|
<TableCell>
|
|
<Checkbox
|
|
checked={selectedIds.has(question.id)}
|
|
onCheckedChange={() => toggleSelection(question.id)}
|
|
aria-label={`Select question ${index + 1}`}
|
|
/>
|
|
</TableCell>
|
|
<TableCell className="font-medium text-muted-foreground">
|
|
{index + 1}
|
|
</TableCell>
|
|
<TableCell>
|
|
<span className="line-clamp-2">{question.text}</span>
|
|
</TableCell>
|
|
<TableCell>
|
|
{question.partner_text ? (
|
|
<Badge variant="secondary" className="text-xs">
|
|
<Users className="h-3 w-3 mr-1" />
|
|
Two-Part
|
|
</Badge>
|
|
) : (
|
|
<span className="text-muted-foreground text-xs">—</span>
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
{question.allowed_couple_genders && question.allowed_couple_genders.length > 0 ? (
|
|
<div className="flex flex-wrap gap-1">
|
|
{question.allowed_couple_genders.map(g => (
|
|
<Badge key={g} variant="outline" className="text-[10px] px-1 py-0 h-5">
|
|
{g.replace('male+male', 'M+M').replace('female+male', 'M+F').replace('female+female', 'F+F')}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<span className="text-muted-foreground text-xs">All</span>
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
{question.target_user_genders && question.target_user_genders.length > 0 ? (
|
|
<div className="flex flex-wrap gap-1">
|
|
{question.target_user_genders.map(g => (
|
|
<Badge key={g} variant="outline" className="text-[10px] px-1 py-0 h-5 border-orange-500 text-orange-500">
|
|
{g.charAt(0).toUpperCase()}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<span className="text-muted-foreground text-xs">Any</span>
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge
|
|
className={`${getIntensity(question.intensity).color} text-white`}
|
|
title={getIntensity(question.intensity).description}
|
|
>
|
|
{getIntensity(question.intensity).label}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="flex gap-1">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => openEditDialog(question)}
|
|
>
|
|
<Pencil className="h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => handleDelete(question)}
|
|
>
|
|
<Trash2 className="h-4 w-4 text-destructive" />
|
|
</Button>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
)
|
|
}
|
|
</div >
|
|
);
|
|
}
|