chore(admin-web): clear feedback warnings

What changed:

- Removed unused feedback router import and unused catch bindings.

- Wrapped feedback loading in useCallback to satisfy hook dependencies.

- Replaced screenshot lightbox img with unoptimized Next Image.

Warning impact:

- dashboards/admin-web feedback page: 7 warnings -> 0 warnings.

- Workspace lint warning lines: 191 -> 176 in /tmp/lint-before-agent-a.log and /tmp/lint-after-fix-agent-a.log.

Verification:

- pnpm --filter @bytelyst/admin-web typecheck

- pnpm --filter @bytelyst/admin-web test

- pnpm --filter @bytelyst/admin-web exec eslint . --ext .ts,.tsx

- pnpm lint
This commit is contained in:
Saravana Achu Mac 2026-05-04 15:58:49 -07:00
parent 021f053143
commit db4257fd76

View File

@ -1,7 +1,7 @@
'use client';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import Image from 'next/image';
import { useCallback, useEffect, useState } from 'react';
import {
Search,
MessageSquare,
@ -26,12 +26,7 @@ import {
TableHeader,
TableRow,
} from '@/components/ui/table';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import {
Select,
SelectContent,
@ -94,22 +89,22 @@ export default function FeedbackPage() {
const [lightboxOpen, setLightboxOpen] = useState(false);
const { toast } = useToast();
useEffect(() => {
fetchFeedback();
}, []);
async function fetchFeedback() {
const fetchFeedback = useCallback(async () => {
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) {
} catch {
toast({ title: 'Error', description: 'Failed to load feedback', variant: 'error' });
} finally {
setLoading(false);
}
}
}, [toast]);
useEffect(() => {
fetchFeedback();
}, [fetchFeedback]);
async function viewScreenshot(feedbackId: string) {
try {
@ -118,7 +113,7 @@ export default function FeedbackPage() {
const data = await res.json();
setScreenshotUrl(data.url);
setLightboxOpen(true);
} catch (err) {
} catch {
toast({ title: 'Error', description: 'Failed to load screenshot', variant: 'error' });
}
}
@ -130,7 +125,7 @@ export default function FeedbackPage() {
if (!res.ok) throw new Error('Failed to delete');
toast({ title: 'Success', description: 'Screenshot deleted' });
fetchFeedback();
} catch (err) {
} catch {
toast({ title: 'Error', description: 'Failed to delete screenshot', variant: 'error' });
}
}
@ -140,7 +135,7 @@ export default function FeedbackPage() {
const res = await fetch(`/api/feedback/${feedbackId}/screenshot`);
if (!res.ok) throw new Error('Failed to fetch screenshot');
const data = await res.json();
// Create temporary link to download
const link = document.createElement('a');
link.href = data.url;
@ -148,19 +143,21 @@ export default function FeedbackPage() {
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} catch (err) {
} catch {
toast({ title: 'Error', description: 'Failed to download screenshot', variant: 'error' });
}
}
const filteredFeedback = feedback.filter((f) => {
const matchesSearch = f.title.toLowerCase().includes(search.toLowerCase()) ||
f.body?.toLowerCase().includes(search.toLowerCase());
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);
const matchesScreenshot =
screenshotFilter === 'all' ||
(screenshotFilter === 'has' && f.screenshotBlobPath) ||
(screenshotFilter === 'none' && !f.screenshotBlobPath);
return matchesSearch && matchesType && matchesStatus && matchesScreenshot;
});
@ -169,7 +166,9 @@ export default function FeedbackPage() {
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-2xl font-bold">User Feedback</h1>
<p className="text-muted-foreground">View and manage user-submitted feedback with screenshots</p>
<p className="text-muted-foreground">
View and manage user-submitted feedback with screenshots
</p>
</div>
</div>
@ -181,7 +180,7 @@ export default function FeedbackPage() {
<Input
placeholder="Search feedback..."
value={search}
onChange={(e) => setSearch(e.target.value)}
onChange={e => setSearch(e.target.value)}
className="pl-9"
/>
</div>
@ -246,7 +245,7 @@ export default function FeedbackPage() {
</TableRow>
</TableHeader>
<TableBody>
{filteredFeedback.map((f) => {
{filteredFeedback.map(f => {
const TypeIcon = typeIcons[f.type];
return (
<TableRow key={f.id}>
@ -276,11 +275,7 @@ export default function FeedbackPage() {
<TableCell>{new Date(f.createdAt).toLocaleDateString()}</TableCell>
<TableCell>
<div className="flex gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => setSelectedFeedback(f)}
>
<Button variant="ghost" size="sm" onClick={() => setSelectedFeedback(f)}>
<Eye className="h-4 w-4" />
</Button>
{f.screenshotBlobPath && (
@ -312,9 +307,7 @@ export default function FeedbackPage() {
{selectedFeedback && (
<div className="space-y-4">
<div className="flex items-center gap-2">
<Badge className={typeColors[selectedFeedback.type]}>
{selectedFeedback.type}
</Badge>
<Badge className={typeColors[selectedFeedback.type]}>{selectedFeedback.type}</Badge>
<Badge className={statusColors[selectedFeedback.status]}>
{selectedFeedback.status}
</Badge>
@ -327,24 +320,23 @@ export default function FeedbackPage() {
<div className="border rounded-lg p-4">
<h4 className="font-medium mb-2">Screenshot</h4>
<div className="flex gap-2">
<Button
variant="outline"
onClick={() => viewScreenshot(selectedFeedback.id)}
>
<Button variant="outline" onClick={() => viewScreenshot(selectedFeedback.id)}>
<Eye className="h-4 w-4 mr-2" />
View
</Button>
<Button
variant="outline"
onClick={() => downloadScreenshot(selectedFeedback.id, selectedFeedback.screenshotContentType)}
onClick={() =>
downloadScreenshot(
selectedFeedback.id,
selectedFeedback.screenshotContentType
)
}
>
<Download className="h-4 w-4 mr-2" />
Download
</Button>
<Button
variant="outline"
onClick={() => deleteScreenshot(selectedFeedback.id)}
>
<Button variant="outline" onClick={() => deleteScreenshot(selectedFeedback.id)}>
<Trash2 className="h-4 w-4 mr-2 text-red-500" />
Delete
</Button>
@ -358,19 +350,34 @@ export default function FeedbackPage() {
<h4 className="font-medium mb-2">Device Context</h4>
<div className="grid grid-cols-2 gap-2 text-sm">
{selectedFeedback.deviceContext.osVersion && (
<div><span className="text-muted-foreground">OS:</span> {selectedFeedback.deviceContext.osVersion}</div>
<div>
<span className="text-muted-foreground">OS:</span>{' '}
{selectedFeedback.deviceContext.osVersion}
</div>
)}
{selectedFeedback.deviceContext.appVersion && (
<div><span className="text-muted-foreground">App:</span> {selectedFeedback.deviceContext.appVersion}</div>
<div>
<span className="text-muted-foreground">App:</span>{' '}
{selectedFeedback.deviceContext.appVersion}
</div>
)}
{selectedFeedback.deviceContext.deviceModel && (
<div><span className="text-muted-foreground">Device:</span> {selectedFeedback.deviceContext.deviceModel}</div>
<div>
<span className="text-muted-foreground">Device:</span>{' '}
{selectedFeedback.deviceContext.deviceModel}
</div>
)}
{selectedFeedback.deviceContext.screenResolution && (
<div><span className="text-muted-foreground">Resolution:</span> {selectedFeedback.deviceContext.screenResolution}</div>
<div>
<span className="text-muted-foreground">Resolution:</span>{' '}
{selectedFeedback.deviceContext.screenResolution}
</div>
)}
{selectedFeedback.deviceContext.locale && (
<div><span className="text-muted-foreground">Locale:</span> {selectedFeedback.deviceContext.locale}</div>
<div>
<span className="text-muted-foreground">Locale:</span>{' '}
{selectedFeedback.deviceContext.locale}
</div>
)}
</div>
</div>
@ -384,11 +391,16 @@ export default function FeedbackPage() {
<Dialog open={lightboxOpen} onOpenChange={setLightboxOpen}>
<DialogContent className="max-w-4xl p-0 overflow-hidden">
{screenshotUrl && (
<img
src={screenshotUrl}
alt="Screenshot"
className="w-full h-auto max-h-[80vh] object-contain"
/>
<div className="relative h-[80vh] w-full">
<Image
src={screenshotUrl}
alt="Screenshot"
fill
sizes="100vw"
unoptimized
className="object-contain"
/>
</div>
)}
</DialogContent>
</Dialog>