diff --git a/apps/web/src/components/tools/image-to-base64-results.tsx b/apps/web/src/components/tools/image-to-base64-results.tsx index 30c7b2a0..0ccb6625 100644 --- a/apps/web/src/components/tools/image-to-base64-results.tsx +++ b/apps/web/src/components/tools/image-to-base64-results.tsx @@ -1,7 +1,8 @@ -import { Check, ChevronDown, ChevronRight, ClipboardCopy, Download, Loader2 } from "lucide-react"; +import { Check, ClipboardCopy, Download, FileJson, FileText, Loader2 } from "lucide-react"; import { useCallback, useState } from "react"; import type { Base64Result } from "@/stores/base64-store"; import { useBase64Store } from "@/stores/base64-store"; +import { useFileStore } from "@/stores/file-store"; // -- Snippet generators ----------------------------------------------------- @@ -52,6 +53,16 @@ function formatBytes(bytes: number): string { return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } +function downloadFile(content: string, filename: string, type: string) { + const blob = new Blob([content], { type }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); +} + // -- CopyButton ------------------------------------------------------------- function CopyButton({ text, label }: { text: string; label?: string }) { @@ -75,7 +86,7 @@ function CopyButton({ text, label }: { text: string; label?: string }) { ); } -// -- Single file result ----------------------------------------------------- +// -- Single file result view ------------------------------------------------ function FileResult({ result }: { result: Base64Result }) { const [activeTab, setActiveTab] = useState("datauri"); @@ -154,118 +165,161 @@ function FileResult({ result }: { result: Base64Result }) { ); } -// -- Batch accordion item --------------------------------------------------- - -function BatchItem({ - result, - expanded, - onToggle, -}: { - result: Base64Result; - expanded: boolean; - onToggle: () => void; -}) { - return ( -
- - {expanded && ( -
- -
- )} -
- ); -} - // -- Main ResultsPanel ------------------------------------------------------ export function ImageToBase64Results() { - const { results, errors, processing, expandedIndex, setExpandedIndex } = useBase64Store(); + const { results, errors, processing, progress } = useBase64Store(); + const { entries, selectedIndex, originalBlobUrl, selectedFileName } = useFileStore(); + // -- Processing state: progress bar -- if (processing) { + const pct = + progress && progress.total > 0 ? Math.round((progress.completed / progress.total) * 100) : 0; + return (
-
+
-

Converting to base64...

+ {progress ? ( + <> +

+ Converting{" "} + {progress.currentFile} +

+
+
+
+
+

+ {progress.completed} of {progress.total} files +

+
+ + ) : ( +

Starting conversion...

+ )}
); } - if (results.length === 0) { + // -- No results yet: show preview of selected image -- + if (results.length === 0 && errors.length === 0) { + if (originalBlobUrl) { + return ( +
+ {selectedFileName +

+ {entries.length > 1 + ? `${entries.length} files ready. Click "Convert to Base64" to start.` + : `Click "Convert to Base64" to convert.`} +

+
+ ); + } + return (
-
-

- Upload images and click "Convert to Base64" to get started. -

-
-
- ); - } - - // Single file - show directly - if (results.length === 1 && errors.length === 0) { - return ( -
- -
- ); - } - - // Batch - accordion view - return ( -
-
-

- {results.length} converted - {errors.length > 0 ? `, ${errors.length} failed` : ""} +

+ Upload images and click "Convert to Base64" to get started.

- r.dataUri), - null, - 2, - )} - label="Copy All as JSON" - />
+ ); + } - {errors.map((err) => ( -
-

- {err.filename}: {err.error} -

+ // -- Results ready: find result for the currently selected file -- + const currentFileName = entries[selectedIndex]?.file.name ?? null; + const currentResult = currentFileName + ? results.find((r) => r.filename === currentFileName) + : null; + const currentError = currentFileName ? errors.find((e) => e.filename === currentFileName) : null; + + const hasMultiple = entries.length > 1; + + return ( +
+ {/* Batch summary bar */} + {hasMultiple && ( +
+
+

+ {results.length} of {entries.length} converted + {errors.length > 0 ? ` - ${errors.length} failed` : ""} +

+
+
+ r.dataUri), + null, + 2, + )} + label="Copy All as JSON" + /> + + +
- ))} + )} - {results.map((result, i) => ( - setExpandedIndex(expandedIndex === i ? -1 : i)} - /> - ))} + {/* Current file result */} +
+ {currentResult ? ( + + ) : currentError ? ( +
+
+

{currentError.filename}

+

{currentError.error}

+
+
+ ) : ( +
+

+ No result for this file. It may not have been processed yet. +

+
+ )} +
); } diff --git a/apps/web/src/components/tools/image-to-base64-settings.tsx b/apps/web/src/components/tools/image-to-base64-settings.tsx index 7d45105f..21c3faf5 100644 --- a/apps/web/src/components/tools/image-to-base64-settings.tsx +++ b/apps/web/src/components/tools/image-to-base64-settings.tsx @@ -13,7 +13,7 @@ const OUTPUT_FORMATS = [ export function ImageToBase64Settings() { const { files } = useFileStore(); - const { processing, setProcessing, setResults, reset } = useBase64Store(); + const { processing, setProcessing, setProgress, addResult, addError, reset } = useBase64Store(); const [outputFormat, setOutputFormat] = useState("original"); const [quality, setQuality] = useState(80); @@ -27,32 +27,44 @@ export function ImageToBase64Settings() { setProcessing(true); setError(null); reset(); + setProcessing(true); - try { - const formData = new FormData(); - for (const file of files) { + const settings = JSON.stringify({ outputFormat, quality, maxWidth, maxHeight }); + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + setProgress({ completed: i, total: files.length, currentFile: file.name }); + + try { + const formData = new FormData(); formData.append("files", file); + formData.append("settings", settings); + + const res = await fetch("/api/v1/tools/image-to-base64", { + method: "POST", + headers: formatHeaders(), + body: formData, + }); + + if (!res.ok) { + const body = await res.json().catch(() => ({})); + addError({ filename: file.name, error: body.error || `Failed: ${res.status}` }); + continue; + } + + const data = await res.json(); + for (const r of data.results) addResult(r); + for (const e of data.errors) addError(e); + } catch (err) { + addError({ + filename: file.name, + error: err instanceof Error ? err.message : "Failed to convert", + }); } - formData.append("settings", JSON.stringify({ outputFormat, quality, maxWidth, maxHeight })); - - const res = await fetch("/api/v1/tools/image-to-base64", { - method: "POST", - headers: formatHeaders(), - body: formData, - }); - - if (!res.ok) { - const body = await res.json().catch(() => ({})); - throw new Error(body.error || `Failed: ${res.status}`); - } - - const data = await res.json(); - setResults(data.results, data.errors); - } catch (err) { - setError(err instanceof Error ? err.message : "Failed to convert"); - } finally { - setProcessing(false); } + + setProgress(null); + setProcessing(false); }; const hasFiles = files.length > 0; diff --git a/apps/web/src/stores/base64-store.ts b/apps/web/src/stores/base64-store.ts index 370da835..6de07b27 100644 --- a/apps/web/src/stores/base64-store.ts +++ b/apps/web/src/stores/base64-store.ts @@ -17,14 +17,24 @@ export interface Base64Error { error: string; } +export interface Base64Progress { + completed: number; + total: number; + currentFile: string; +} + interface Base64State { results: Base64Result[]; errors: Base64Error[]; processing: boolean; + progress: Base64Progress | null; expandedIndex: number; setResults: (results: Base64Result[], errors: Base64Error[]) => void; setProcessing: (v: boolean) => void; + setProgress: (p: Base64Progress | null) => void; + addResult: (result: Base64Result) => void; + addError: (error: Base64Error) => void; setExpandedIndex: (i: number) => void; reset: () => void; } @@ -33,10 +43,15 @@ export const useBase64Store = create((set) => ({ results: [], errors: [], processing: false, + progress: null, expandedIndex: 0, setResults: (results, errors) => set({ results, errors, expandedIndex: 0 }), setProcessing: (v) => set({ processing: v }), + setProgress: (p) => set({ progress: p }), + addResult: (result) => set((s) => ({ results: [...s.results, result] })), + addError: (error) => set((s) => ({ errors: [...s.errors, error] })), setExpandedIndex: (i) => set({ expandedIndex: i }), - reset: () => set({ results: [], errors: [], processing: false, expandedIndex: 0 }), + reset: () => + set({ results: [], errors: [], processing: false, progress: null, expandedIndex: 0 }), }));