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