import { useState, useEffect, useCallback } from 'react'; import { supabase } from '@/config'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; import { Skeleton } from '@/components/ui/skeleton'; import { PaginationControls } from '@/components/ui/pagination'; import { Textarea } from '@/components/ui/textarea'; import { useRealtimeSubscription } from '@/hooks/useRealtimeSubscription'; import { RealtimeStatusIndicator } from '@/components/RealtimeStatusIndicator'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Label } from "@/components/ui/label"; import { Search, Bug, Lightbulb, MessageSquare, HelpCircle, ExternalLink, Smartphone, User } from 'lucide-react'; import { format } from 'date-fns'; import { toast } from 'sonner'; import { Link } from 'react-router-dom'; type FeedbackType = 'bug' | 'feature_request' | 'general' | 'question'; type FeedbackStatus = 'new' | 'reviewed' | 'in_progress' | 'resolved' | 'closed'; interface Feedback { id: string; user_id: string; type: FeedbackType; title: string; description: string; screenshot_url: string | null; device_info: Record; status: FeedbackStatus; admin_notes: string | null; question_id: string | null; created_at: string; updated_at: string; profile?: { id: string; name: string | null; email: string | null; }; question?: { id: string; text: string; pack?: { name: string; }; }; } const feedbackTypeConfig: Record = { bug: { label: 'Bug', icon: , variant: 'destructive' }, feature_request: { label: 'Feature', icon: , variant: 'default' }, general: { label: 'General', icon: , variant: 'secondary' }, question: { label: 'Question', icon: , variant: 'outline' }, }; const statusConfig: Record = { new: { label: 'New', variant: 'destructive' }, reviewed: { label: 'Reviewed', variant: 'secondary' }, in_progress: { label: 'In Progress', variant: 'default' }, resolved: { label: 'Resolved', variant: 'outline' }, closed: { label: 'Closed', variant: 'outline' }, }; export function FeedbackPage() { const [feedback, setFeedback] = useState([]); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(''); const [typeFilter, setTypeFilter] = useState('all'); const [statusFilter, setStatusFilter] = useState('all'); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(25); // Detail Dialog State const [selectedFeedback, setSelectedFeedback] = useState(null); const [isUpdating, setIsUpdating] = useState(false); const [editStatus, setEditStatus] = useState('new'); const [editNotes, setEditNotes] = useState(''); const fetchFeedback = useCallback(async () => { setLoading(true); try { const { data, error } = await supabase .from('feedback') .select(` *, profile:profiles!feedback_user_id_fkey(id, name, email), question:questions!feedback_question_id_fkey(id, text, pack:question_packs(name)) `) .order('created_at', { ascending: false }); if (error) throw error; setFeedback(data || []); } catch (error: unknown) { console.error('Failed to load feedback:', error); toast.error("Failed to load feedback"); } finally { setLoading(false); } }, []); // Real-time subscription for feedback table const { status: realtimeStatus } = useRealtimeSubscription({ table: 'feedback', onInsert: useCallback(() => { // Refetch to get the full data with profile/question relations fetchFeedback(); }, [fetchFeedback]), onUpdate: useCallback(({ new: updated }: { old: Feedback; new: Feedback }) => { setFeedback(prev => prev.map(f => f.id === updated.id ? { ...f, ...updated } : f)); }, []), onDelete: useCallback((deleted: Feedback) => { setFeedback(prev => prev.filter(f => f.id !== deleted.id)); }, []), insertToast: { enabled: true, message: (payload) => `New ${payload.type.replace('_', ' ')} feedback: "${payload.title}"`, type: 'info', }, }); useEffect(() => { fetchFeedback(); }, [fetchFeedback]); useEffect(() => { setPage(1); }, [search, typeFilter, statusFilter]); const handleOpenDetail = (item: Feedback) => { setSelectedFeedback(item); setEditStatus(item.status); setEditNotes(item.admin_notes || ''); }; const handleUpdateFeedback = async () => { if (!selectedFeedback) return; setIsUpdating(true); try { const { error } = await supabase .from('feedback') .update({ status: editStatus, admin_notes: editNotes.trim() || null, updated_at: new Date().toISOString(), }) .eq('id', selectedFeedback.id); if (error) throw error; toast.success("Feedback updated"); setSelectedFeedback(null); fetchFeedback(); } catch (error: any) { console.error('Failed to update feedback:', error); toast.error("Failed to update feedback"); } finally { setIsUpdating(false); } }; const filteredFeedback = feedback.filter(item => { if (typeFilter !== 'all' && item.type !== typeFilter) return false; if (statusFilter !== 'all' && item.status !== statusFilter) return false; if (!search) return true; const query = search.toLowerCase(); return ( item.title.toLowerCase().includes(query) || item.description.toLowerCase().includes(query) || item.profile?.name?.toLowerCase().includes(query) || item.profile?.email?.toLowerCase().includes(query) ); }); const totalCount = filteredFeedback.length; const paginatedFeedback = filteredFeedback.slice( (page - 1) * pageSize, page * pageSize ); useEffect(() => { const totalPages = Math.max(1, Math.ceil(totalCount / pageSize)); if (page > totalPages) { setPage(totalPages); } }, [page, pageSize, totalCount]); const getTypeBadge = (type: FeedbackType) => { const config = feedbackTypeConfig[type]; return ( {config.icon} {config.label} ); }; const getStatusBadge = (status: FeedbackStatus) => { const config = statusConfig[status]; return {config.label}; }; const formatDeviceInfo = (info: Record) => { if (!info || Object.keys(info).length === 0) return null; const lines: string[] = []; if (info.platform) lines.push(`Platform: ${info.platform}`); if (info.osVersion) lines.push(`OS Version: ${info.osVersion}`); if (info.appVersion) lines.push(`App Version: ${info.appVersion}`); if (info.deviceModel) lines.push(`Device: ${info.deviceModel}`); return lines.length > 0 ? lines.join('\n') : JSON.stringify(info, null, 2); }; return (

User Feedback

Review and respond to user feedback submissions

{/* Filters */}
setSearch(e.target.value)} className="pl-9" />
{/* Table */}
Type Title User Status Created Actions {loading ? (
) : filteredFeedback.length === 0 ? ( {search || typeFilter !== 'all' || statusFilter !== 'all' ? 'No feedback matches your filters.' : 'No feedback submissions yet.'} ) : ( paginatedFeedback.map((item) => ( handleOpenDetail(item)}> {getTypeBadge(item.type)}
{item.title}
{item.description.slice(0, 80)} {item.description.length > 80 && '...'}
{item.profile?.name || 'Unknown'}
{item.profile?.email}
{getStatusBadge(item.status)} {format(new Date(item.created_at), 'MMM d, yyyy')}
)) )}
{filteredFeedback.length > 0 && ( { setPage(1); setPageSize(size); }} /> )} {/* Detail Dialog */} !open && setSelectedFeedback(null)}> {selectedFeedback && getTypeBadge(selectedFeedback.type)} {selectedFeedback?.title} Submitted {selectedFeedback && format(new Date(selectedFeedback.created_at), 'MMMM d, yyyy \'at\' h:mm a')} {selectedFeedback && (
{/* User Info */}
{selectedFeedback.profile?.name || 'Unknown User'}
{selectedFeedback.profile?.email}
{selectedFeedback.profile?.id && ( e.stopPropagation()} > View user profile )}
{/* Description */}

{selectedFeedback.description}

{/* Related Question */} {selectedFeedback.question && (

{selectedFeedback.question.text}

{selectedFeedback.question.pack && (

From pack: {selectedFeedback.question.pack.name}

)}
)} {/* Screenshot */} {selectedFeedback.screenshot_url && (
Screenshot
)} {/* Device Info */} {selectedFeedback.device_info && Object.keys(selectedFeedback.device_info).length > 0 && (
                                            {formatDeviceInfo(selectedFeedback.device_info)}
                                        
)} {/* Status Update */}