import { useEffect, useState, useCallback } from 'react'; import { Link, useParams } from 'react-router-dom'; import { adminData } from '@/lib/adminApi'; import { auditedAdminData } from '@/hooks/useAuditedAdminData'; import { useEntityForm } from '@/hooks/useEntityForm'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Switch } from '@/components/ui/switch'; import { IconPicker, IconPreview } from '@/components/ui/icon-picker'; import { Skeleton } from '@/components/ui/skeleton'; import { PaginationControls } from '@/components/ui/pagination'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Plus, Zap, Pencil, Trash2, Loader2, Crown, Eye, EyeOff, ChevronUp, ChevronDown } from 'lucide-react'; import { toast } from 'sonner'; import { ContentReviewControl } from '@/components/content/ContentReviewControl'; import type { ContentReviewStatus } from '@sauci/shared'; import { parseIntensityRange } from '@/lib/dareIntensity'; // ============================================================================= // Types // ============================================================================= interface Category { id: string; name: string; icon?: string | null; } interface DarePack { id: string; name: string; description: string | null; icon: string | null; is_premium: boolean; is_public: boolean; is_explicit: boolean; sort_order: number; category_id: string | null; min_intensity: number | null; max_intensity: number | null; avg_intensity: number | null; created_at: string | null; dare_count?: number; content_status?: ContentReviewStatus | null; content_review_reason?: string | null; } interface DarePackFormData { name: string; description: string; icon: string; is_premium: boolean; is_public: boolean; is_explicit: boolean; category_id: string; min_intensity: string; max_intensity: string; } // ============================================================================= // Component // ============================================================================= export function DarePacksPage() { const { categoryId } = useParams<{ categoryId: string }>(); // Data state const [darePacks, setDarePacks] = useState([]); const [categories, setCategories] = useState([]); const [category, setCategory] = useState(null); const [loading, setLoading] = useState(true); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(12); const [totalCount, setTotalCount] = useState(0); // Form state using the hook const form = useEntityForm( { name: '', description: '', icon: 'flame-outline', is_premium: false, is_public: false, is_explicit: false, category_id: categoryId ?? '', min_intensity: '', max_intensity: '', }, (pack) => ({ name: pack.name, description: pack.description || '', icon: pack.icon || 'flame-outline', is_premium: pack.is_premium, is_public: pack.is_public, is_explicit: pack.is_explicit, category_id: pack.category_id || '', min_intensity: pack.min_intensity != null ? String(pack.min_intensity) : '', max_intensity: pack.max_intensity != null ? String(pack.max_intensity) : '', }) ); // ============================================================================= // Data Fetching // ============================================================================= const fetchData = useCallback(async () => { setLoading(true); try { const from = (page - 1) * pageSize; const to = from + pageSize - 1; // Fetch category info if (categoryId) { const { data: categoryData } = await adminData .from('categories') .select('id, name, icon') .eq('id', categoryId) .single(); setCategory(categoryData || null); } else { setCategory(null); } // Fetch dare packs let query = adminData .from('dare_packs') .select('*', { count: 'exact' }) .order('sort_order', { ascending: true }) .range(from, to); if (categoryId) { query = query.eq('category_id', categoryId); } const { data: packData, error, count } = await query; if (error) throw error; setTotalCount(count || 0); // Fetch dare counts const packIds = (packData || []).map(p => p.id); const dareCounts: Record = {}; if (packIds.length > 0) { const { data: dares } = await adminData .from('dares') .select('pack_id') .in('pack_id', packIds); dares?.forEach(d => { dareCounts[d.pack_id] = (dareCounts[d.pack_id] || 0) + 1; }); } // Fetch categories const { data: categoryData } = await adminData .from('categories') .select('id, name') .order('name'); setCategories(categoryData || []); setDarePacks( (packData || []).map(p => ({ ...p, dare_count: dareCounts[p.id] || 0, })) ); } catch (error) { toast.error('Failed to load dare packs'); console.error(error); } finally { setLoading(false); } }, [page, pageSize, categoryId]); useEffect(() => { fetchData(); }, [fetchData]); useEffect(() => { setPage(1); }, [categoryId]); useEffect(() => { const totalPages = Math.max(1, Math.ceil(totalCount / pageSize)); if (page > totalPages) { setPage(totalPages); } }, [page, pageSize, totalCount]); // ============================================================================= // Handlers // ============================================================================= const handleSave = async () => { if (!form.formData.name.trim()) { toast.error('Name is required'); return; } const { range, error: intensityError } = parseIntensityRange(form.formData.min_intensity, form.formData.max_intensity); if (intensityError) { toast.error(intensityError); return; } const parsedMin = range.min; const parsedMax = range.max; form.setSaving(true); try { if (form.editingItem) { const { error } = await auditedAdminData.update('dare_packs', form.editingItem.id, { name: form.formData.name, description: form.formData.description || null, icon: form.formData.icon || null, is_premium: form.formData.is_premium, is_public: form.formData.is_public, is_explicit: form.formData.is_explicit, category_id: form.formData.category_id || null, min_intensity: parsedMin, max_intensity: parsedMax, }); if (error) throw error; toast.success('Dare pack updated'); } else { const { error } = await auditedAdminData.insert('dare_packs', { name: form.formData.name, description: form.formData.description || null, icon: form.formData.icon || null, is_premium: form.formData.is_premium, is_public: form.formData.is_public, is_explicit: form.formData.is_explicit, category_id: form.formData.category_id || null, min_intensity: parsedMin, max_intensity: parsedMax, sort_order: totalCount, }); if (error) throw error; toast.success('Dare pack created'); } form.close(); fetchData(); } catch (error) { toast.error('Failed to save dare pack'); console.error(error); } finally { form.setSaving(false); } }; const handleDelete = async (pack: DarePack) => { if (!confirm(`Delete dare pack "${pack.name}"? This will delete all dares in this pack.`)) { return; } try { const { error } = await auditedAdminData.delete('dare_packs', pack.id); if (error) throw error; toast.success('Dare pack deleted'); fetchData(); } catch (error) { toast.error('Failed to delete dare pack'); console.error(error); } }; const handleMove = async (pack: DarePack, direction: 'up' | 'down') => { const currentIndex = darePacks.findIndex(p => p.id === pack.id); const targetIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1; if (targetIndex < 0 || targetIndex >= darePacks.length) return; const targetPack = darePacks[targetIndex]; try { const currentOrder = pack.sort_order ?? currentIndex; const targetOrder = targetPack.sort_order ?? targetIndex; const [currentUpdate, targetUpdate] = await Promise.all([ auditedAdminData.update('dare_packs', pack.id, { sort_order: targetOrder }), auditedAdminData.update('dare_packs', targetPack.id, { sort_order: currentOrder }), ]); if (currentUpdate.error || targetUpdate.error) { throw currentUpdate.error ?? targetUpdate.error ?? new Error('Failed to update dare pack order'); } // Optimistic update const newPacks = [...darePacks]; newPacks[currentIndex] = { ...targetPack, sort_order: currentOrder }; newPacks[targetIndex] = { ...pack, sort_order: targetOrder }; setDarePacks(newPacks); } catch (error) { toast.error('Failed to reorder dare packs'); console.error(error); fetchData(); } }; // ============================================================================= // Render // ============================================================================= if (loading) { return (
{[1, 2, 3].map(i => ( ))}
); } return (
{/* Header */}

{category ? ( <> {category.name} ) : ( <> Dare Packs )}

{category ? `Manage dare packs in the ${category.name} category` : 'Manage dare packs that users can send to their partners'}

{form.isEditing ? 'Edit Dare Pack' : 'Create Dare Pack'} {form.isEditing ? 'Update the dare pack details below.' : 'Add a new dare pack for couples to challenge each other.'}
form.setField('icon', iconName)} />
form.setField('name', e.target.value)} placeholder="e.g., Romantic Dares" />