fix(feedback): address 6 bugs/gaps from systematic review

- Fix search icon positioning (add relative container)
- Fix toast variant from 'destructive' to 'error'
- Remove non-existent screenshotUrl fields from delete endpoint
- Fix misleading blob path comment (blob stays at initial location)
- Add download screenshot function and button
- Add pointer-events-none to search icon
This commit is contained in:
saravanakumardb1 2026-03-03 07:03:27 -08:00
parent 439610cbe5
commit 8614e3f0f1
2 changed files with 37 additions and 14 deletions

View File

@ -12,6 +12,7 @@ import {
Trash2, Trash2,
ImageIcon, ImageIcon,
Loader2, Loader2,
Download,
} from 'lucide-react'; } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
@ -104,7 +105,7 @@ export default function FeedbackPage() {
const data = await res.json(); const data = await res.json();
setFeedback(data.items || []); setFeedback(data.items || []);
} catch (err) { } catch (err) {
toast({ title: 'Error', description: 'Failed to load feedback', variant: 'destructive' }); toast({ title: 'Error', description: 'Failed to load feedback', variant: 'error' });
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -118,7 +119,7 @@ export default function FeedbackPage() {
setScreenshotUrl(data.url); setScreenshotUrl(data.url);
setLightboxOpen(true); setLightboxOpen(true);
} catch (err) { } catch (err) {
toast({ title: 'Error', description: 'Failed to load screenshot', variant: 'destructive' }); toast({ title: 'Error', description: 'Failed to load screenshot', variant: 'error' });
} }
} }
@ -130,7 +131,25 @@ export default function FeedbackPage() {
toast({ title: 'Success', description: 'Screenshot deleted' }); toast({ title: 'Success', description: 'Screenshot deleted' });
fetchFeedback(); fetchFeedback();
} catch (err) { } catch (err) {
toast({ title: 'Error', description: 'Failed to delete screenshot', variant: 'destructive' }); toast({ title: 'Error', description: 'Failed to delete screenshot', variant: 'error' });
}
}
async function downloadScreenshot(feedbackId: string, contentType?: 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();
// Create temporary link to download
const link = document.createElement('a');
link.href = data.url;
link.download = `screenshot-${feedbackId}.${contentType?.split('/')[1] || 'png'}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} catch (err) {
toast({ title: 'Error', description: 'Failed to download screenshot', variant: 'error' });
} }
} }
@ -157,8 +176,8 @@ export default function FeedbackPage() {
<Card className="mb-6"> <Card className="mb-6">
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="flex flex-wrap gap-4"> <div className="flex flex-wrap gap-4">
<div className="flex-1 min-w-[200px]"> <div className="flex-1 min-w-[200px] relative">
<Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" /> <Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground pointer-events-none" />
<Input <Input
placeholder="Search feedback..." placeholder="Search feedback..."
value={search} value={search}
@ -313,14 +332,21 @@ export default function FeedbackPage() {
onClick={() => viewScreenshot(selectedFeedback.id)} onClick={() => viewScreenshot(selectedFeedback.id)}
> >
<Eye className="h-4 w-4 mr-2" /> <Eye className="h-4 w-4 mr-2" />
View Screenshot View
</Button>
<Button
variant="outline"
onClick={() => downloadScreenshot(selectedFeedback.id, selectedFeedback.screenshotContentType)}
>
<Download className="h-4 w-4 mr-2" />
Download
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
onClick={() => deleteScreenshot(selectedFeedback.id)} 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 Screenshot Delete
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -95,10 +95,9 @@ export async function feedbackRoutes(app: FastifyInstance): Promise<void> {
throw new BadRequestError('Invalid contentType. Must be image/png, image/jpeg, or image/webp'); throw new BadRequestError('Invalid contentType. Must be image/png, image/jpeg, or image/webp');
} }
// Generate blob path (feedbackId will be assigned after creation) // Generate blob path with user-specific prefix for organization
// For pre-upload, we use a temp path that gets moved on feedback submission // Note: Blob stays at this location - we don't move it after feedback creation
const tempFeedbackId = `temp_${userId}_${Date.now()}`; const blobPath = generateScreenshotBlobPath(productId, userId, body.contentType);
const blobPath = generateScreenshotBlobPath(productId, tempFeedbackId, body.contentType);
// Generate SAS URL for upload (5 minutes expiry) // Generate SAS URL for upload (5 minutes expiry)
const uploadUrl = await generateSasUrl( const uploadUrl = await generateSasUrl(
@ -155,10 +154,8 @@ export async function feedbackRoutes(app: FastifyInstance): Promise<void> {
} }
// Update feedback to remove screenshot reference // Update feedback to remove screenshot reference
const updated = await updateFeedback(req.params.id, productId, { await updateFeedback(req.params.id, productId, {
screenshotBlobPath: undefined, screenshotBlobPath: undefined,
screenshotUrl: undefined,
screenshotUrlExpiresAt: undefined,
screenshotContentType: undefined, screenshotContentType: undefined,
screenshotSizeBytes: undefined, screenshotSizeBytes: undefined,
} as unknown as import('./types.js').UpdateFeedbackInput); } as unknown as import('./types.js').UpdateFeedbackInput);