diff --git a/README.md b/README.md index 1e66186..bce7f4e 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,12 @@ -# Next.js template +# Image Optimizer -This is a Next.js template with shadcn/ui. +# Description +Image Optimizer is a web tool that offers different options to compress, resize, and optimize your images. -## Adding components +# Tech Stack -To add components to your app, run the following command: +
+ +
-```bash -npx shadcn@latest add button -``` - -This will place the ui components in the `components` directory. - -## Using components - -To use the components in your app, import them as follows: - -```tsx -import { Button } from "@/components/ui/button"; -``` +This tool is built using *Next.js* (App Router) and React. \ No newline at end of file diff --git a/app/edit/page.tsx b/app/edit/page.tsx index 4664ede..00b85cf 100644 --- a/app/edit/page.tsx +++ b/app/edit/page.tsx @@ -3,6 +3,8 @@ import React, { useEffect } from "react" import Image from "next/image" import { useRouter } from "next/navigation" import { useImageStore } from "../../data/imageStore" +import ImageDiffViewer from "@/components/image-diff-viewer" +import ImageEditSettings from "@/components/image-edit-settings" export default function EditPage() { const router = useRouter() @@ -13,24 +15,14 @@ export default function EditPage() { if (!file) router.replace("/") }, [file, router]) - if (!file) return
No image loaded. Redirecting…
- - return ( -
-

-
- {url ? ( - uploaded - ) : ( -

Preparing image…

- )} -
+ return url === null ? null : ( +
+ +
) } diff --git a/components/image-diff-viewer.tsx b/components/image-diff-viewer.tsx new file mode 100644 index 0000000..8b06eb2 --- /dev/null +++ b/components/image-diff-viewer.tsx @@ -0,0 +1,45 @@ +"use client" +import React from "react" +import { + ReactCompareSlider, + ReactCompareSliderImage, + ReactCompareSliderHandle, +} from "react-compare-slider" + +type ImageDiffViewerProps = { + beforeSrc: string + afterSrc: string + alt?: string + className?: string +} + +export default function ImageDiffViewer({ + beforeSrc, + afterSrc, + alt = "", + className = "", +}: ImageDiffViewerProps) { + if (!beforeSrc || !afterSrc) return null + + return ( +
+ + } + itemTwo={ + + } + handle={} + /> +
+ ) +} diff --git a/components/image-edit-settings.tsx b/components/image-edit-settings.tsx new file mode 100644 index 0000000..583fa2f --- /dev/null +++ b/components/image-edit-settings.tsx @@ -0,0 +1,7 @@ +export default function ImageEditSettings() { + return ( +
+ +
+ ) +} \ No newline at end of file diff --git a/components/image-upload.tsx b/components/image-upload.tsx index 5224824..df257de 100644 --- a/components/image-upload.tsx +++ b/components/image-upload.tsx @@ -17,7 +17,7 @@ export default function ImageUpload() { const inputRef = useRef(null) const timersRef = useRef([]) const router = useRouter() - const setImageFile = useImageStore((s) => s.setFile) + const setOriginalFile = useImageStore((s) => s.setOriginalFile) const errorWaitTime = 2000 // ms const successWaitTime = 1500 // ms @@ -82,7 +82,7 @@ export default function ImageUpload() { } try { - setImageFile(file) + setOriginalFile(file) if (inputRef.current) inputRef.current.value = "" setStatus("success") pushTimeout(() => { diff --git a/data/imageStore.ts b/data/imageStore.ts index 6c098f4..f713c94 100644 --- a/data/imageStore.ts +++ b/data/imageStore.ts @@ -1,29 +1,96 @@ import create from "zustand" type ImageState = { + // Working copy (modifiable by editors/libraries) file: File | null url: string | null - setFile: (file: File) => void + + // Original uploaded file (kept immutable) + originalFile: File | null + originalUrl: string | null + + // Set the original file (called at upload time). This will also + // create a working copy so editors can modify `file` without + // mutating the original. + setOriginalFile: (file: File) => void + + // Replace the working copy (e.g. when a library returns a modified Blob) + setWorkingFile: (fileOrBlob: File | Blob) => void + + // Reset the working copy back to the original + resetWorking: () => void + clear: () => void + + // Helpers to get ArrayBuffers from working/original getArrayBuffer: () => Promise + getOriginalArrayBuffer: () => Promise } export const useImageStore = create((set, get) => ({ file: null, url: null, + originalFile: null, + originalUrl: null, + + setOriginalFile: (file: File) => { + // revoke previous original URL if present + const prevOriginal = get().originalUrl + if (prevOriginal) URL.revokeObjectURL(prevOriginal) + const originalUrl = URL.createObjectURL(file) + + // revoke previous working URL if present + const prevWorking = get().url + if (prevWorking) URL.revokeObjectURL(prevWorking) + + // create a new working copy (so modifications don't touch the original) + const workingCopy = new File([file], file.name, { type: file.type }) + const workingUrl = URL.createObjectURL(workingCopy) + + set({ originalFile: file, originalUrl, file: workingCopy, url: workingUrl }) + }, - setFile: (file: File) => { - // Revoke any previous object URL to avoid memory leaks + setWorkingFile: (fileOrBlob: File | Blob) => { const prev = get().url if (prev) URL.revokeObjectURL(prev) - const url = URL.createObjectURL(file) - set({ file, url }) + + let newFile: File + if (fileOrBlob instanceof File) { + newFile = fileOrBlob + } else { + const name = get().originalFile?.name ?? "edited" + const type = + (fileOrBlob as Blob).type || + get().originalFile?.type || + "application/octet-stream" + newFile = new File([fileOrBlob], name, { type }) + } + + const url = URL.createObjectURL(newFile) + set({ file: newFile, url }) + }, + + resetWorking: () => { + const original = get().originalFile + const prev = get().url + if (prev) URL.revokeObjectURL(prev) + if (!original) { + set({ file: null, url: null }) + return + } + const workingCopy = new File([original], original.name, { + type: original.type, + }) + const workingUrl = URL.createObjectURL(workingCopy) + set({ file: workingCopy, url: workingUrl }) }, clear: () => { + const prevOriginal = get().originalUrl + if (prevOriginal) URL.revokeObjectURL(prevOriginal) const prev = get().url if (prev) URL.revokeObjectURL(prev) - set({ file: null, url: null }) + set({ file: null, url: null, originalFile: null, originalUrl: null }) }, getArrayBuffer: async () => { @@ -31,4 +98,10 @@ export const useImageStore = create((set, get) => ({ if (!f) return null return f.arrayBuffer() }, + + getOriginalArrayBuffer: async () => { + const f = get().originalFile + if (!f) return null + return f.arrayBuffer() + }, })) diff --git a/package-lock.json b/package-lock.json index 4d7a4d6..250eadd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,11 +14,13 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^12.40.0", + "i": "^0.3.7", "lucide-react": "^1.17.0", "next": "16.2.6", "next-themes": "^0.4.6", "radix-ui": "^1.4.3", "react": "19.2.4", + "react-compare-slider": "^4.0.0", "react-dom": "19.2.4", "shadcn": "^4.8.2", "tailwind-merge": "^3.6.0", @@ -457,9 +459,9 @@ } }, "node_modules/@dotenvx/dotenvx": { - "version": "1.69.1", - "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.69.1.tgz", - "integrity": "sha512-kwQB5KcAegxw/+NGUgXAo5ovyOSjlMhoXSSnSEpDhoHJwzMcMO0HE1U0VCYZ7jbAeCMGamed9XdWzOA5ixtTNg==", + "version": "1.69.2", + "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.69.2.tgz", + "integrity": "sha512-60Vh31k5bAAPq57Isx+Iopl2FrZQzZyUc4kh/OESgXiwhrKXqdxEn3JGTGSawng1DkkeC4jofFq1VpPJqNQx4w==", "license": "BSD-3-Clause", "dependencies": { "commander": "^11.1.0", @@ -1041,6 +1043,9 @@ "cpu": [ "arm" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1057,6 +1062,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1073,6 +1081,9 @@ "cpu": [ "ppc64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1089,6 +1100,9 @@ "cpu": [ "riscv64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1105,6 +1119,9 @@ "cpu": [ "s390x" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1121,6 +1138,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1137,6 +1157,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1153,6 +1176,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1169,6 +1195,9 @@ "cpu": [ "arm" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1191,6 +1220,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1213,6 +1245,9 @@ "cpu": [ "ppc64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1235,6 +1270,9 @@ "cpu": [ "riscv64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1257,6 +1295,9 @@ "cpu": [ "s390x" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1279,6 +1320,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1301,6 +1345,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1323,6 +1370,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1415,25 +1465,25 @@ } }, "node_modules/@inquirer/ansi": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.6.tgz", - "integrity": "sha512-I/INw4sHGlVZ/afZOckpLiDP9SmbMl1g/GCqeHjLw1Afw/0PlRs2tRFgTGWmdI0hoNuWZn3y2iHNmG1vyECyQQ==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.7.tgz", + "integrity": "sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==", "license": "MIT", "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" } }, "node_modules/@inquirer/confirm": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.1.0.tgz", - "integrity": "sha512-USpeB76eqK7yGricDlGAupxWlp4a59qpeZOoNWaxO/nJln7agpJveyNkQ1d5u8YXG6TOqxZtQpKPORQQDrdVsA==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.1.1.tgz", + "integrity": "sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ==", "license": "MIT", "dependencies": { - "@inquirer/core": "^11.2.0", - "@inquirer/type": "^4.0.6" + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -1445,21 +1495,21 @@ } }, "node_modules/@inquirer/core": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.2.0.tgz", - "integrity": "sha512-joR1YS2sI0us+9d0I8ViqFbrRLONO8CFTuyvBX4ZVBSch+VsZiugUABdrhBXXJR1VyEzvpz5SQCix3keETQ58g==", + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.2.1.tgz", + "integrity": "sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA==", "license": "MIT", "dependencies": { - "@inquirer/ansi": "^2.0.6", - "@inquirer/figures": "^2.0.6", - "@inquirer/type": "^4.0.6", + "@inquirer/ansi": "^2.0.7", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", - "mute-stream": "^4.0.0", + "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -1471,21 +1521,21 @@ } }, "node_modules/@inquirer/figures": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.6.tgz", - "integrity": "sha512-dsZgQtH2t5Q6ah3aPbZbeEZAxsD9qQu0DXf01AltuEfRTm+NoLN6+rLVbr+4edeEbNCp/wBNM6mALRWtsQpfkw==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.7.tgz", + "integrity": "sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==", "license": "MIT", "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" } }, "node_modules/@inquirer/type": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.6.tgz", - "integrity": "sha512-J+9tdxOskuYuGjsvGaq00AamhDgjR7anhEW2dP4QdQpFCMPngCeC/bCYWQ5NsMWZRdsy53is7kAHb/+7cwDk2g==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.7.tgz", + "integrity": "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==", "license": "MIT", "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -1700,6 +1750,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1716,6 +1769,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1732,6 +1788,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1748,6 +1807,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3560,6 +3622,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3577,6 +3642,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3594,6 +3662,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3611,6 +3682,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -4256,6 +4330,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4270,6 +4347,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -4284,6 +4364,9 @@ "loong64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4298,6 +4381,9 @@ "loong64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -4312,6 +4398,9 @@ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4326,6 +4415,9 @@ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4340,6 +4432,9 @@ "riscv64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -4354,6 +4449,9 @@ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4368,6 +4466,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4382,6 +4483,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -4858,9 +4962,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.32", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz", - "integrity": "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==", + "version": "2.10.33", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.33.tgz", + "integrity": "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -5616,9 +5720,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.363", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.363.tgz", - "integrity": "sha512-VjUKPyWzGnT1fujlkEGC/BvN70Hh70KXtAqcmniXviYlJC/ivcT+BWGPyxWVbJZLfvtKR6dqg1L7T7pgAMBtWA==", + "version": "1.5.364", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.364.tgz", + "integrity": "sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -7159,6 +7263,14 @@ "node": ">=18.18.0" } }, + "node_modules/i": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/i/-/i-0.3.7.tgz", + "integrity": "sha512-FYz4wlXgkQwIPqhzC5TdNMLSE5+GS1IIDJZY/1ZiEPCT2S3COUVZeT5OW4BmW4r5LHLQuOosSwsvnroG9GR59Q==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/iconv-lite": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", @@ -8173,6 +8285,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8194,6 +8309,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8215,6 +8333,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8236,6 +8357,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8606,12 +8730,12 @@ } }, "node_modules/mute-stream": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-4.0.0.tgz", - "integrity": "sha512-gSrprq0fJ3EiOErzjdIZrjysVVmJ4uu1QWfCDss5LypA5OXvrMje5Ym5z6V6RLyJ2eF87lasX7t6a0AnFvZblg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", "license": "ISC", "engines": { - "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/nanoid": { @@ -9672,6 +9796,19 @@ "node": ">=0.10.0" } }, + "node_modules/react-compare-slider": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/react-compare-slider/-/react-compare-slider-4.0.0.tgz", + "integrity": "sha512-XNpwfWa8OHvJZIsC+ywhjpRWX8VCY8fIavQ82R63Tever1pQndhicr0ecDuU69GjA+YjesoV7LX3QnKsECtMDw==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-dom": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", @@ -10157,9 +10294,9 @@ "license": "ISC" }, "node_modules/shadcn": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/shadcn/-/shadcn-4.8.2.tgz", - "integrity": "sha512-pt3KneOg6LGYKNAdoTVf/lpVcf7t2MlV+Ll2Xc3lIIYN3ph4ajrjU+CcG6OVSgO5ubbLZj+9j5oMA9Lqg7o8KA==", + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/shadcn/-/shadcn-4.8.3.tgz", + "integrity": "sha512-cnP485rIqtDb8waOp+IKUIfifVf64/PCd5VX/nnLeIP+qZ3yS4r6FOQtOdQkoQEvomQUsJS2OHr5CTzKWw0wKQ==", "license": "MIT", "dependencies": { "@babel/core": "^7.28.0", @@ -10860,21 +10997,21 @@ } }, "node_modules/tldts": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.0.tgz", - "integrity": "sha512-yHBe+zVfzNZ3QfTPW/Z6KK1G2t340gFjMHqI/4KKSt/abzYydzuCnpqdaF5gCCABby+9Yfbj59oR5F2Fd5CBzg==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.2.tgz", + "integrity": "sha512-kCwffuaH8ntKtygnWe1b4BJKWiCUH30n5KfoTr6IchcXOwR7chAOFJxFrH3vjANafUYrIA4a7SDL+nn7SiR4Sw==", "license": "MIT", "dependencies": { - "tldts-core": "^7.4.0" + "tldts-core": "^7.4.2" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.1.tgz", - "integrity": "sha512-sc2nGvGbixlJRHwTh/qQdPXTxJU1UDJboGPQm4d/01YUJ9r/u6aeIulQvEaxUlvKDN7hb1qCLjax+jhVAPLa/g==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.2.tgz", + "integrity": "sha512-nwEyF4vl4RSJjwSjBUmOSxc3BFPoIFdlRthJ6e+5v9P3bHNsoD06UjuqMUspqp7vsEZ1beaHi1km+optiE17yA==", "license": "MIT" }, "node_modules/to-regex-range": { diff --git a/package.json b/package.json index 1a850fe..eea53b0 100644 --- a/package.json +++ b/package.json @@ -15,11 +15,13 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^12.40.0", + "i": "^0.3.7", "lucide-react": "^1.17.0", "next": "16.2.6", "next-themes": "^0.4.6", "radix-ui": "^1.4.3", "react": "19.2.4", + "react-compare-slider": "^4.0.0", "react-dom": "19.2.4", "shadcn": "^4.8.2", "tailwind-merge": "^3.6.0", diff --git a/public/hackerman.png b/public/hackerman.png new file mode 100644 index 0000000..6854661 Binary files /dev/null and b/public/hackerman.png differ