Files
SnapOtter/apps/web/src/components/common/review-panel.tsx
T

197 lines
6.3 KiB
TypeScript

import { AlertCircle, ArrowLeft, CheckCircle2, Download, FileText } from "lucide-react";
import { useMemo } from "react";
import { Link } from "react-router-dom";
import { useTranslation } from "@/contexts/i18n-context";
import { formatFileSize, triggerDownload } from "@/lib/download";
import { format } from "@/lib/format";
/** Tools whose primary output is text/data, not a downloadable file. */
const DATA_OUTPUT_TOOLS = new Set([
"ocr",
"barcode-read",
"info",
"histogram",
"color-palette",
"transcribe-audio",
"extract-subtitles",
"image-to-base64",
"pdf-to-text",
"pdf-metadata",
"audio-metadata",
"video-metadata",
]);
/** Tools that produce multiple output files bundled as a ZIP. */
const MULTI_OUTPUT_TOOLS = new Set([
"split",
"favicon",
"pdf-to-image",
"video-to-frames",
"split-audio",
"split-csv",
]);
interface ReviewPanelProps {
filename: string;
fileSize: number;
fileType: string;
originalSize: number;
downloadUrl: string;
onUndo: () => void;
onStartOver: () => void;
currentToolId: string;
totalCount?: number;
successCount?: number;
failedCount?: number;
}
export function ReviewPanel({
filename,
fileSize,
fileType,
originalSize,
downloadUrl,
onUndo,
onStartOver,
currentToolId,
totalCount,
successCount,
failedCount,
}: ReviewPanelProps) {
const { t } = useTranslation();
const isDataOutput = DATA_OUTPUT_TOOLS.has(currentToolId);
const isMultiOutput = MULTI_OUTPUT_TOOLS.has(currentToolId);
const sizeDelta = useMemo(() => {
if (!originalSize || originalSize === 0) return 0;
return Math.round((1 - fileSize / originalSize) * 100);
}, [originalSize, fileSize]);
const handleDownload = () => {
triggerDownload(downloadUrl, filename);
};
const hasBatchStats =
totalCount != null && totalCount > 1 && successCount != null && failedCount != null;
return (
<div className="space-y-3">
<div className="border-t border-border" />
{/* Success indicator */}
<div className="flex items-center gap-2">
<CheckCircle2 className="h-4 w-4 text-emerald-600 shrink-0" />
<span className="text-sm font-medium text-foreground">{t.toolPage.conversionComplete}</span>
</div>
{/* Batch partial failure summary */}
{hasBatchStats && failedCount > 0 && (
<div className="flex items-start gap-2 rounded-lg bg-amber-50 dark:bg-amber-950/30 p-2.5 text-xs">
<AlertCircle className="h-3.5 w-3.5 text-amber-600 dark:text-amber-400 shrink-0 mt-0.5" />
<span className="text-amber-800 dark:text-amber-300">
{format(t.toolPage.batchPartialSuccess, {
success: successCount,
total: totalCount,
failed: failedCount,
})}
</span>
</div>
)}
{/* Size delta -- hidden for data-output tools */}
{!isDataOutput && originalSize > 0 && (
<div className="space-y-1 text-xs">
<div className="flex justify-between">
<span className="text-muted-foreground">{t.toolPage.original}</span>
<span className="tabular-nums text-foreground">{formatFileSize(originalSize)}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">{t.toolPage.processed}</span>
<span className="tabular-nums text-foreground">{formatFileSize(fileSize)}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">{t.toolPage.saved}</span>
<span
className={`tabular-nums font-medium ${
sizeDelta > 0
? "text-emerald-600"
: sizeDelta === 0
? "text-muted-foreground"
: "text-foreground"
}`}
>
{sizeDelta === 0
? t.toolPage.noChange
: sizeDelta > 0
? `-${sizeDelta}%`
: `+${Math.abs(sizeDelta)}%`}
</span>
</div>
</div>
)}
{/* Data-output tools: results hint + secondary download */}
{isDataOutput && (
<>
<div className="flex items-start gap-2 rounded-lg bg-muted/50 p-2.5 text-xs">
<FileText className="h-3.5 w-3.5 text-muted-foreground shrink-0 mt-0.5" />
<span className="text-muted-foreground">{t.toolPage.dataResultsHint}</span>
</div>
<button
type="button"
onClick={handleDownload}
className="w-full text-center text-xs text-primary hover:text-primary/80 underline underline-offset-2"
>
{t.toolPage.downloadAsFile}
</button>
</>
)}
{/* Download button -- primary for non-data tools */}
{!isDataOutput && (
<button
type="button"
data-download-button
onClick={handleDownload}
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium text-sm flex items-center justify-center gap-2 hover:bg-primary/90"
>
<Download className="h-4 w-4" />
{isMultiOutput
? `${t.toolPage.downloadAll} (ZIP, ${formatFileSize(fileSize)})`
: hasBatchStats && successCount != null && successCount > 1
? `${format(t.toolPage.downloadFiles, { count: successCount })} (ZIP, ${formatFileSize(fileSize)})`
: `${t.toolPage.download} ${fileType} (${formatFileSize(fileSize)})`}
</button>
)}
{/* Adjust settings */}
<button
type="button"
onClick={onUndo}
className="w-full py-2 rounded-lg border border-border text-foreground hover:bg-muted text-sm font-medium"
>
{t.toolPage.adjustSettings}
</button>
{/* Text links */}
<div className="flex flex-col items-center gap-1.5 text-xs">
<button
type="button"
onClick={onStartOver}
className="text-muted-foreground hover:text-foreground"
>
{t.toolPage.startOver}
</button>
<Link
to="/"
className="text-muted-foreground hover:text-foreground flex items-center gap-1"
>
<ArrowLeft className="h-3 w-3" />
{t.toolPage.backToTools}
</Link>
</div>
</div>
);
}