From 271cab647bb41872c5e205d6236e35eb7fad0810 Mon Sep 17 00:00:00 2001 From: Siddharth Kumar Sah Date: Mon, 23 Mar 2026 12:47:06 +0800 Subject: [PATCH] feat: add SideBySideComparison component for resize results --- .../common/side-by-side-comparison.tsx | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 apps/web/src/components/common/side-by-side-comparison.tsx diff --git a/apps/web/src/components/common/side-by-side-comparison.tsx b/apps/web/src/components/common/side-by-side-comparison.tsx new file mode 100644 index 00000000..f484e960 --- /dev/null +++ b/apps/web/src/components/common/side-by-side-comparison.tsx @@ -0,0 +1,116 @@ +import { useState } from "react"; + +interface SideBySideComparisonProps { + beforeSrc: string; + afterSrc: string; + beforeSize?: number; + afterSize?: number; +} + +function formatSize(bytes: number): string { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; +} + +export function SideBySideComparison({ + beforeSrc, + afterSrc, + beforeSize, + afterSize, +}: SideBySideComparisonProps) { + const [beforeDims, setBeforeDims] = useState<{ w: number; h: number } | null>(null); + const [afterDims, setAfterDims] = useState<{ w: number; h: number } | null>(null); + + const savingsPercent = + beforeSize && afterSize && beforeSize > 0 + ? ((1 - afterSize / beforeSize) * 100).toFixed(1) + : null; + + const checkerboard = { + backgroundImage: `linear-gradient(45deg, #ccc 25%, transparent 25%), + linear-gradient(-45deg, #ccc 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, #ccc 75%), + linear-gradient(-45deg, transparent 75%, #ccc 75%)`, + backgroundSize: "16px 16px", + backgroundPosition: "0 0, 0 8px, 8px -8px, -8px 0px", + }; + + return ( +
+ {/* Side-by-side images */} +
+ {/* Original */} +
+ + Original + +
+ Original { + const img = e.currentTarget; + setBeforeDims({ w: img.naturalWidth, h: img.naturalHeight }); + }} + /> +
+
+ {beforeDims && ( +

+ {beforeDims.w} × {beforeDims.h} +

+ )} + {beforeSize != null &&

{formatSize(beforeSize)}

} +
+
+ + {/* Resized */} +
+ + Resized + +
+ Resized { + const img = e.currentTarget; + setAfterDims({ w: img.naturalWidth, h: img.naturalHeight }); + }} + /> +
+
+ {afterDims && ( +

+ {afterDims.w} × {afterDims.h} +

+ )} + {afterSize != null &&

{formatSize(afterSize)}

} +
+
+
+ + {/* Size savings */} + {savingsPercent !== null && ( +

0 ? "text-green-600 dark:text-green-400" : "text-red-500"}`} + > + {Number(savingsPercent) > 0 + ? `${savingsPercent}% smaller` + : `${Math.abs(Number(savingsPercent))}% larger`} +

+ )} +
+ ); +}