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:
parent
021f053143
commit
db4257fd76
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user