diff --git a/dashboards/admin-web/src/app/(dashboard)/feedback/page.tsx b/dashboards/admin-web/src/app/(dashboard)/feedback/page.tsx new file mode 100644 index 00000000..0fdfdea8 --- /dev/null +++ b/dashboards/admin-web/src/app/(dashboard)/feedback/page.tsx @@ -0,0 +1,371 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { + Search, + MessageSquare, + Bug, + Lightbulb, + ThumbsUp, + Eye, + Trash2, + ImageIcon, + Loader2, +} from 'lucide-react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { useToast } from '@/components/ui/toast'; + +interface Feedback { + id: string; + type: 'bug' | 'feature' | 'praise' | 'other'; + title: string; + body?: string; + status: 'new' | 'reviewed' | 'resolved' | 'closed'; + userId: string; + userEmail?: string; + createdAt: string; + screenshotBlobPath?: string; + screenshotContentType?: string; + deviceContext?: { + osVersion?: string; + appVersion?: string; + deviceModel?: string; + screenResolution?: string; + locale?: string; + }; +} + +const typeIcons = { + bug: Bug, + feature: Lightbulb, + praise: ThumbsUp, + other: MessageSquare, +}; + +const typeColors = { + bug: 'bg-red-100 text-red-800', + feature: 'bg-blue-100 text-blue-800', + praise: 'bg-green-100 text-green-800', + other: 'bg-gray-100 text-gray-800', +}; + +const statusColors = { + new: 'bg-yellow-100 text-yellow-800', + reviewed: 'bg-blue-100 text-blue-800', + resolved: 'bg-green-100 text-green-800', + closed: 'bg-gray-100 text-gray-800', +}; + +export default 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 [screenshotFilter, setScreenshotFilter] = useState('all'); + const [selectedFeedback, setSelectedFeedback] = useState(null); + const [screenshotUrl, setScreenshotUrl] = useState(null); + const [lightboxOpen, setLightboxOpen] = useState(false); + const { toast } = useToast(); + + useEffect(() => { + fetchFeedback(); + }, []); + + async function fetchFeedback() { + try { + const res = await fetch('/api/feedback'); + if (!res.ok) throw new Error('Failed to fetch'); + const data = await res.json(); + setFeedback(data.items || []); + } catch (err) { + toast({ title: 'Error', description: 'Failed to load feedback', variant: 'destructive' }); + } finally { + setLoading(false); + } + } + + async function viewScreenshot(feedbackId: string) { + try { + const res = await fetch(`/api/feedback/${feedbackId}/screenshot`); + if (!res.ok) throw new Error('Failed to fetch screenshot'); + const data = await res.json(); + setScreenshotUrl(data.url); + setLightboxOpen(true); + } catch (err) { + toast({ title: 'Error', description: 'Failed to load screenshot', variant: 'destructive' }); + } + } + + async function deleteScreenshot(feedbackId: string) { + if (!confirm('Delete this screenshot?')) return; + try { + const res = await fetch(`/api/feedback/${feedbackId}/screenshot`, { method: 'DELETE' }); + if (!res.ok) throw new Error('Failed to delete'); + toast({ title: 'Success', description: 'Screenshot deleted' }); + fetchFeedback(); + } catch (err) { + toast({ title: 'Error', description: 'Failed to delete screenshot', variant: 'destructive' }); + } + } + + const filteredFeedback = feedback.filter((f) => { + const matchesSearch = f.title.toLowerCase().includes(search.toLowerCase()) || + f.body?.toLowerCase().includes(search.toLowerCase()); + const matchesType = typeFilter === 'all' || f.type === typeFilter; + const matchesStatus = statusFilter === 'all' || f.status === statusFilter; + const matchesScreenshot = screenshotFilter === 'all' || + (screenshotFilter === 'has' && f.screenshotBlobPath) || + (screenshotFilter === 'none' && !f.screenshotBlobPath); + return matchesSearch && matchesType && matchesStatus && matchesScreenshot; + }); + + return ( +
+
+
+

User Feedback

+

View and manage user-submitted feedback with screenshots

+
+
+ + + +
+
+ + setSearch(e.target.value)} + className="pl-9" + /> +
+ + + +
+
+
+ + + + Feedback ({filteredFeedback.length}) + + + {loading ? ( +
+ +
+ ) : ( + + + + Type + Title + User + Status + Screenshot + Date + Actions + + + + {filteredFeedback.map((f) => { + const TypeIcon = typeIcons[f.type]; + return ( + + + + + {f.type} + + + + {f.title} + + {f.userEmail || f.userId.slice(0, 8)} + + {f.status} + + + {f.screenshotBlobPath ? ( + + + Yes + + ) : ( + - + )} + + {new Date(f.createdAt).toLocaleDateString()} + +
+ + {f.screenshotBlobPath && ( + + )} +
+
+
+ ); + })} +
+
+ )} +
+
+ + {/* Feedback Detail Dialog */} + setSelectedFeedback(null)}> + + + Feedback Details + + {selectedFeedback && ( +
+
+ + {selectedFeedback.type} + + + {selectedFeedback.status} + +
+

{selectedFeedback.title}

+

{selectedFeedback.body}

+ + {/* Screenshot Section */} + {selectedFeedback.screenshotBlobPath && ( +
+

Screenshot

+
+ + +
+
+ )} + + {/* Device Context */} + {selectedFeedback.deviceContext && ( +
+

Device Context

+
+ {selectedFeedback.deviceContext.osVersion && ( +
OS: {selectedFeedback.deviceContext.osVersion}
+ )} + {selectedFeedback.deviceContext.appVersion && ( +
App: {selectedFeedback.deviceContext.appVersion}
+ )} + {selectedFeedback.deviceContext.deviceModel && ( +
Device: {selectedFeedback.deviceContext.deviceModel}
+ )} + {selectedFeedback.deviceContext.screenResolution && ( +
Resolution: {selectedFeedback.deviceContext.screenResolution}
+ )} + {selectedFeedback.deviceContext.locale && ( +
Locale: {selectedFeedback.deviceContext.locale}
+ )} +
+
+ )} +
+ )} +
+
+ + {/* Screenshot Lightbox */} + + + {screenshotUrl && ( + Screenshot + )} + + +
+ ); +}