From 3e0e672089af07fff21fd942f0f7e05e6a81d5bd Mon Sep 17 00:00:00 2001
From: Ayush Lad
Date: Mon, 20 Apr 2026 00:00:58 +0530
Subject: [PATCH 1/3] feat: add batch processing with ZIP download to 6 PDF
tools | NSoC'26
---
.env | 1 +
package-lock.json | 885 +++++++++++++++++---------
package.json | 2 +-
src/components/pdf/BatchPanel.jsx | 169 +++++
src/components/ui/BatchToggle.jsx | 38 ++
src/hooks/useBatchProcess.js | 91 +++
src/pages/Compress/Compress.jsx | 45 +-
src/pages/Grayscale/Grayscale.jsx | 40 ++
src/pages/LockPdf/LockPdf.jsx | 54 +-
src/pages/PageNumbers/PageNumbers.jsx | 61 +-
src/pages/Rotate/Rotate.jsx | 49 +-
src/pages/Watermark/Watermark.jsx | 61 +-
12 files changed, 1173 insertions(+), 323 deletions(-)
create mode 100644 .env
create mode 100644 src/components/pdf/BatchPanel.jsx
create mode 100644 src/components/ui/BatchToggle.jsx
create mode 100644 src/hooks/useBatchProcess.js
diff --git a/.env b/.env
new file mode 100644
index 0000000..828f118
--- /dev/null
+++ b/.env
@@ -0,0 +1 @@
+VITE_WALLETCONNECT_PROJECT_ID=placeholder_project_id
diff --git a/package-lock.json b/package-lock.json
index 594525e..f0ce01f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,7 +17,7 @@
"jszip": "^3.10.1",
"lucide-react": "^0.577.0",
"pdf-lib": "^1.17.1",
- "pdfjs-dist": "^5.5.207",
+ "pdfjs-dist": "^4.4.168",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-router-dom": "^7.13.1",
@@ -77,7 +77,6 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -421,7 +420,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -829,7 +827,6 @@
"resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.10.tgz",
"integrity": "sha512-PlPhdtjgWUra+LImQTnXOUqUa/jcufZhizdR93ZjlQSS3ahCtDTG6pJw7j0OwFal18DQjICXfeVNsUUrcNisfA==",
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"@firebase/component": "0.7.2",
"@firebase/logger": "0.5.0",
@@ -896,7 +893,6 @@
"resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.10.tgz",
"integrity": "sha512-tFmBuZL0/v1h6eyKRgWI58ucft6dEJmAi9nhPUXoAW4ZbPSTlnsh31AuEwUoRTz+wwRk9gmgss9GZV05ZM9Kug==",
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"@firebase/app": "0.14.10",
"@firebase/component": "0.7.2",
@@ -912,8 +908,7 @@
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz",
"integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==",
- "license": "Apache-2.0",
- "peer": true
+ "license": "Apache-2.0"
},
"node_modules/@firebase/auth": {
"version": "1.12.2",
@@ -1364,7 +1359,6 @@
"integrity": "sha512-AmWf3cHAOMbrCPG4xdPKQaj5iHnyYfyLKZxwz+Xf55bqKbpAmcYifB4jQinT2W9XhDRHISOoPyBOariJpCG6FA==",
"hasInstallScript": true,
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"tslib": "^2.1.0"
},
@@ -1539,6 +1533,40 @@
"@lit-labs/ssr-dom-shim": "^1.5.0"
}
},
+ "node_modules/@mapbox/node-pre-gyp": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
+ "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==",
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "dependencies": {
+ "detect-libc": "^2.0.0",
+ "https-proxy-agent": "^5.0.0",
+ "make-dir": "^3.1.0",
+ "node-fetch": "^2.6.7",
+ "nopt": "^5.0.0",
+ "npmlog": "^5.0.1",
+ "rimraf": "^3.0.2",
+ "semver": "^7.3.5",
+ "tar": "^6.1.11"
+ },
+ "bin": {
+ "node-pre-gyp": "bin/node-pre-gyp"
+ }
+ },
+ "node_modules/@mapbox/node-pre-gyp/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "license": "ISC",
+ "optional": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/@metamask/eth-json-rpc-provider": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@metamask/eth-json-rpc-provider/-/eth-json-rpc-provider-1.0.1.tgz",
@@ -2100,7 +2128,6 @@
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
@@ -2179,256 +2206,6 @@
"uuid": "dist/bin/uuid"
}
},
- "node_modules/@napi-rs/canvas": {
- "version": "0.1.97",
- "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.97.tgz",
- "integrity": "sha512-8cFniXvrIEnVwuNSRCW9wirRZbHvrD3JVujdS2P5n5xiJZNZMOZcfOvJ1pb66c7jXMKHHglJEDVJGbm8XWFcXQ==",
- "license": "MIT",
- "optional": true,
- "workspaces": [
- "e2e/*"
- ],
- "engines": {
- "node": ">= 10"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/Brooooooklyn"
- },
- "optionalDependencies": {
- "@napi-rs/canvas-android-arm64": "0.1.97",
- "@napi-rs/canvas-darwin-arm64": "0.1.97",
- "@napi-rs/canvas-darwin-x64": "0.1.97",
- "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.97",
- "@napi-rs/canvas-linux-arm64-gnu": "0.1.97",
- "@napi-rs/canvas-linux-arm64-musl": "0.1.97",
- "@napi-rs/canvas-linux-riscv64-gnu": "0.1.97",
- "@napi-rs/canvas-linux-x64-gnu": "0.1.97",
- "@napi-rs/canvas-linux-x64-musl": "0.1.97",
- "@napi-rs/canvas-win32-arm64-msvc": "0.1.97",
- "@napi-rs/canvas-win32-x64-msvc": "0.1.97"
- }
- },
- "node_modules/@napi-rs/canvas-android-arm64": {
- "version": "0.1.97",
- "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.97.tgz",
- "integrity": "sha512-V1c/WVw+NzH8vk7ZK/O8/nyBSCQimU8sfMsB/9qeSvdkGKNU7+mxy/bIF0gTgeBFmHpj30S4E9WHMSrxXGQuVQ==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">= 10"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/Brooooooklyn"
- }
- },
- "node_modules/@napi-rs/canvas-darwin-arm64": {
- "version": "0.1.97",
- "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.97.tgz",
- "integrity": "sha512-ok+SCEF4YejcxuJ9Rm+WWunHHpf2HmiPxfz6z1a/NFQECGXtsY7A4B8XocK1LmT1D7P174MzwPF9Wy3AUAwEPw==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 10"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/Brooooooklyn"
- }
- },
- "node_modules/@napi-rs/canvas-darwin-x64": {
- "version": "0.1.97",
- "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.97.tgz",
- "integrity": "sha512-PUP6e6/UGlclUvAQNnuXCcnkpdUou6VYZfQOQxExLp86epOylmiwLkqXIvpFmjoTEDmPmXrI+coL/9EFU1gKPA==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 10"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/Brooooooklyn"
- }
- },
- "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
- "version": "0.1.97",
- "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.97.tgz",
- "integrity": "sha512-XyXH2L/cic8eTNtbrXCcvqHtMX/nEOxN18+7rMrAM2XtLYC/EB5s0wnO1FsLMWmK+04ZSLN9FBGipo7kpIkcOw==",
- "cpu": [
- "arm"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/Brooooooklyn"
- }
- },
- "node_modules/@napi-rs/canvas-linux-arm64-gnu": {
- "version": "0.1.97",
- "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.97.tgz",
- "integrity": "sha512-Kuq/M3djq0K8ktgz6nPlK7Ne5d4uWeDxPpyKWOjWDK2RIOhHVtLtyLiJw2fuldw7Vn4mhw05EZXCEr4Q76rs9w==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/Brooooooklyn"
- }
- },
- "node_modules/@napi-rs/canvas-linux-arm64-musl": {
- "version": "0.1.97",
- "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.97.tgz",
- "integrity": "sha512-kKmSkQVnWeqg7qdsiXvYxKhAFuHz3tkBjW/zyQv5YKUPhotpaVhpBGv5LqCngzyuRV85SXoe+OFj+Tv0a0QXkQ==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/Brooooooklyn"
- }
- },
- "node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
- "version": "0.1.97",
- "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.97.tgz",
- "integrity": "sha512-Jc7I3A51jnEOIAXeLsN/M/+Z28LUeakcsXs07FLq9prXc0eYOtVwsDEv913Gr+06IRo34gJJVgT0TXvmz+N2VA==",
- "cpu": [
- "riscv64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/Brooooooklyn"
- }
- },
- "node_modules/@napi-rs/canvas-linux-x64-gnu": {
- "version": "0.1.97",
- "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.97.tgz",
- "integrity": "sha512-iDUBe7AilfuBSRbSa8/IGX38Mf+iCSBqoVKLSQ5XaY2JLOaqz1TVyPFEyIck7wT6mRQhQt5sN6ogfjIDfi74tg==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/Brooooooklyn"
- }
- },
- "node_modules/@napi-rs/canvas-linux-x64-musl": {
- "version": "0.1.97",
- "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.97.tgz",
- "integrity": "sha512-AKLFd/v0Z5fvgqBDqhvqtAdx+fHMJ5t9JcUNKq4FIZ5WH+iegGm8HPdj00NFlCSnm83Fp3Ln8I2f7uq1aIiWaA==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/Brooooooklyn"
- }
- },
- "node_modules/@napi-rs/canvas-win32-arm64-msvc": {
- "version": "0.1.97",
- "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.97.tgz",
- "integrity": "sha512-u883Yr6A6fO7Vpsy9YE4FVCIxzzo5sO+7pIUjjoDLjS3vQaNMkVzx5bdIpEL+ob+gU88WDK4VcxYMZ6nmnoX9A==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 10"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/Brooooooklyn"
- }
- },
- "node_modules/@napi-rs/canvas-win32-x64-msvc": {
- "version": "0.1.97",
- "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.97.tgz",
- "integrity": "sha512-sWtD2EE3fV0IzN+iiQUqr/Q1SwqWhs2O1FKItFlxtdDkikpEj5g7DKQpY3x55H/MAOnL8iomnlk3mcEeGiUMoQ==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 10"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/Brooooooklyn"
- }
- },
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
@@ -2451,7 +2228,6 @@
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
"integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
"license": "MIT",
- "peer": true,
"engines": {
"node": "^14.21.3 || >=16"
},
@@ -2957,7 +2733,6 @@
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=10.0.0"
},
@@ -2974,6 +2749,17 @@
}
}
},
+ "node_modules/@reown/appkit-controllers/node_modules/zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
"node_modules/@reown/appkit-pay": {
"version": "1.7.8",
"resolved": "https://registry.npmjs.org/@reown/appkit-pay/-/appkit-pay-1.7.8.tgz",
@@ -3330,7 +3116,6 @@
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=10.0.0"
},
@@ -3347,6 +3132,17 @@
}
}
},
+ "node_modules/@reown/appkit-utils/node_modules/zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
"node_modules/@reown/appkit-wallet": {
"version": "1.7.8",
"resolved": "https://registry.npmjs.org/@reown/appkit-wallet/-/appkit-wallet-1.7.8.tgz",
@@ -3655,7 +3451,6 @@
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=10.0.0"
},
@@ -3672,6 +3467,17 @@
}
}
},
+ "node_modules/@reown/appkit/node_modules/zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.0-rc.10",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.10.tgz",
@@ -4362,7 +4168,6 @@
"resolved": "https://registry.npmjs.org/@solana/kit/-/kit-5.5.1.tgz",
"integrity": "sha512-irKUGiV2yRoyf+4eGQ/ZeCRxa43yjFEL1DUI5B0DkcfZw3cr0VJtVJnrG8OtVF01vT0OUfYOcUn6zJW5TROHvQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@solana/accounts": "5.5.1",
"@solana/addresses": "5.5.1",
@@ -5377,7 +5182,6 @@
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.95.2.tgz",
"integrity": "sha512-/wGkvLj/st5Ud1Q76KF1uFxScV7WeqN1slQx5280ycwAyYkIPGaRZAEgHxe3bjirSd5Zpwkj6zNcR4cqYni/ZA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@tanstack/query-core": "5.95.2"
},
@@ -5459,7 +5263,6 @@
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -5648,7 +5451,6 @@
"resolved": "https://registry.npmjs.org/@wagmi/core/-/core-2.22.1.tgz",
"integrity": "sha512-cG/xwQWsBEcKgRTkQVhH29cbpbs/TdcUJVFXCyri3ZknxhMyGv0YEjTcrNpRgt2SaswL1KrvslSNYKKo+5YEAg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"eventemitter3": "5.0.1",
"mipd": "0.0.7",
@@ -6249,7 +6051,6 @@
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=10.0.0"
},
@@ -6266,6 +6067,17 @@
}
}
},
+ "node_modules/@walletconnect/utils/node_modules/zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
"node_modules/@walletconnect/window-getters": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@walletconnect/window-getters/-/window-getters-1.0.1.tgz",
@@ -6297,6 +6109,13 @@
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"license": "0BSD"
},
+ "node_modules/abbrev": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
+ "license": "ISC",
+ "optional": true
+ },
"node_modules/abitype": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz",
@@ -6336,7 +6155,6 @@
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -6354,6 +6172,19 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
+ "node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
"node_modules/agentkeepalive": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
@@ -6432,6 +6263,43 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/aproba": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz",
+ "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==",
+ "license": "ISC",
+ "optional": true
+ },
+ "node_modules/are-we-there-yet": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
+ "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
+ "deprecated": "This package is no longer supported.",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "delegates": "^1.0.0",
+ "readable-stream": "^3.6.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/are-we-there-yet/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -6520,7 +6388,6 @@
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
@@ -6543,7 +6410,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/base-x": {
@@ -6628,7 +6495,7 @@
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@@ -6655,7 +6522,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -6709,7 +6575,6 @@
"integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==",
"hasInstallScript": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
@@ -6804,6 +6669,22 @@
],
"license": "CC-BY-4.0"
},
+ "node_modules/canvas": {
+ "version": "2.11.2",
+ "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz",
+ "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@mapbox/node-pre-gyp": "^1.0.0",
+ "nan": "^2.17.0",
+ "simple-get": "^3.0.3"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/cbw-sdk": {
"name": "@coinbase/wallet-sdk",
"version": "3.9.3",
@@ -6872,6 +6753,16 @@
"url": "https://paulmillr.com/funding/"
}
},
+ "node_modules/chownr": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+ "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
+ "license": "ISC",
+ "optional": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@@ -6913,6 +6804,16 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
+ "node_modules/color-support": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
+ "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
+ "license": "ISC",
+ "optional": true,
+ "bin": {
+ "color-support": "bin.js"
+ }
+ },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -6938,9 +6839,16 @@
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
+ "node_modules/console-control-strings": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
+ "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
+ "license": "ISC",
+ "optional": true
+ },
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -6990,7 +6898,6 @@
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz",
"integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"node-fetch": "^2.7.0"
}
@@ -7140,6 +7047,19 @@
"node": ">=0.10"
}
},
+ "node_modules/decompress-response": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz",
+ "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "mimic-response": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/dedent": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz",
@@ -7220,6 +7140,13 @@
"node": ">=0.4.0"
}
},
+ "node_modules/delegates": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+ "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/derive-valtio": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/derive-valtio/-/derive-valtio-0.1.0.tgz",
@@ -7245,7 +7172,7 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
- "dev": true,
+ "devOptional": true,
"license": "Apache-2.0",
"engines": {
"node": ">=8"
@@ -7308,7 +7235,6 @@
"resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.18.tgz",
"integrity": "sha512-wG99Zcfcys9fZux7Cft8BAX/YrOJLJSZ3jyYPfhZHqN2E+Ffx+QXBDsv3gubEgPtV6dTzJMSQUwk1H98/t/0wQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@ecies/ciphers": "^0.2.5",
"@noble/ciphers": "^1.3.0",
@@ -7498,7 +7424,6 @@
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -7862,8 +7787,7 @@
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz",
"integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/eventemitter3": {
"version": "5.0.1",
@@ -8205,6 +8129,46 @@
}
}
},
+ "node_modules/fs-minipass": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
+ "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/fs-minipass/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fs-minipass/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "license": "ISC",
+ "optional": true
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "license": "ISC",
+ "optional": true
+ },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -8229,6 +8193,28 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/gauge": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
+ "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
+ "deprecated": "This package is no longer supported.",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "aproba": "^1.0.3 || ^2.0.0",
+ "color-support": "^1.1.2",
+ "console-control-strings": "^1.0.0",
+ "has-unicode": "^2.0.1",
+ "object-assign": "^4.1.1",
+ "signal-exit": "^3.0.0",
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1",
+ "wide-align": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/generator-function": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
@@ -8303,6 +8289,28 @@
"node": ">= 0.4"
}
},
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -8414,6 +8422,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/has-unicode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
+ "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
+ "license": "ISC",
+ "optional": true
+ },
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -8458,6 +8473,20 @@
"integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==",
"license": "MIT"
},
+ "node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/humanize-ms": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
@@ -8542,6 +8571,18 @@
"node": ">=0.8.19"
}
},
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@@ -9354,6 +9395,22 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
+ "node_modules/make-dir": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+ "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "semver": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -9410,11 +9467,24 @@
"node": ">= 0.6"
}
},
+ "node_modules/mimic-response": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz",
+ "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
- "dev": true,
+ "devOptional": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@@ -9423,6 +9493,50 @@
"node": "*"
}
},
+ "node_modules/minipass": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
+ "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
+ "license": "ISC",
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minizlib": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
+ "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "minipass": "^3.0.0",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minizlib/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minizlib/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "license": "ISC",
+ "optional": true
+ },
"node_modules/mipd": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/mipd/-/mipd-0.0.7.tgz",
@@ -9443,6 +9557,19 @@
}
}
},
+ "node_modules/mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "license": "MIT",
+ "optional": true,
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/modern-ahocorasick": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/modern-ahocorasick/-/modern-ahocorasick-1.1.0.tgz",
@@ -9476,6 +9603,13 @@
"integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==",
"license": "(Apache-2.0 AND MIT)"
},
+ "node_modules/nan": {
+ "version": "2.26.2",
+ "resolved": "https://registry.npmjs.org/nan/-/nan-2.26.2.tgz",
+ "integrity": "sha512-0tTvBTYkt3tdGw22nrAy50x7gpbGCCFH3AFcyS5WiUu7Eu4vWlri1woE6qHBSfy11vksDqkiwjOnlR7WV8G1Hw==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -9551,13 +9685,6 @@
"integrity": "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==",
"license": "MIT"
},
- "node_modules/node-readable-to-web-readable-stream": {
- "version": "0.4.2",
- "resolved": "https://registry.npmjs.org/node-readable-to-web-readable-stream/-/node-readable-to-web-readable-stream-0.4.2.tgz",
- "integrity": "sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ==",
- "license": "MIT",
- "optional": true
- },
"node_modules/node-releases": {
"version": "2.0.36",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz",
@@ -9565,6 +9692,22 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/nopt": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
+ "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "abbrev": "1"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -9574,6 +9717,20 @@
"node": ">=0.10.0"
}
},
+ "node_modules/npmlog": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
+ "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
+ "deprecated": "This package is no longer supported.",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "are-we-there-yet": "^2.0.0",
+ "console-control-strings": "^1.1.0",
+ "gauge": "^3.0.0",
+ "set-blocking": "^2.0.0"
+ }
+ },
"node_modules/obj-multiplex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/obj-multiplex/-/obj-multiplex-1.0.0.tgz",
@@ -9585,6 +9742,16 @@
"readable-stream": "^2.3.3"
}
},
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/ofetch": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz",
@@ -9743,6 +9910,16 @@
"node": ">=8"
}
},
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -9753,12 +9930,21 @@
"node": ">=8"
}
},
+ "node_modules/path2d": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/path2d/-/path2d-0.2.2.tgz",
+ "integrity": "sha512-+vnG6S4dYcYxZd+CZxzXCNKdELYZSKfohrk98yajCo1PtRoDgCTrrwOvK1GT0UoAdVszagDVllQc0U1vaX4NUQ==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/pdf-lib": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz",
"integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@pdf-lib/standard-fonts": "^1.0.0",
"@pdf-lib/upng": "^1.0.1",
@@ -9773,16 +9959,16 @@
"license": "0BSD"
},
"node_modules/pdfjs-dist": {
- "version": "5.5.207",
- "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.5.207.tgz",
- "integrity": "sha512-WMqqw06w1vUt9ZfT0gOFhMf3wHsWhaCrxGrckGs5Cci6ybDW87IvPaOd2pnBwT6BJuP/CzXDZxjFgmSULLdsdw==",
+ "version": "4.4.168",
+ "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.4.168.tgz",
+ "integrity": "sha512-MbkAjpwka/dMHaCfQ75RY1FXX3IewBVu6NGZOcxerRFlaBiIkZmUoR0jotX5VUzYZEXAGzSFtknWs5xRKliXPA==",
"license": "Apache-2.0",
"engines": {
- "node": ">=20.19.0 || >=22.13.0 || >=24"
+ "node": ">=18"
},
"optionalDependencies": {
- "@napi-rs/canvas": "^0.1.95",
- "node-readable-to-web-readable-stream": "^0.4.2"
+ "canvas": "^2.11.2",
+ "path2d": "^0.2.0"
}
},
"node_modules/picocolors": {
@@ -9797,7 +9983,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -9984,7 +10169,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -10278,7 +10462,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -10288,7 +10471,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -10465,6 +10647,23 @@
"node": ">=4"
}
},
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "deprecated": "Rimraf versions prior to v4 are no longer supported",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/rolldown": {
"version": "1.0.0-rc.10",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.10.tgz",
@@ -10593,7 +10792,7 @@
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "dev": true,
+ "devOptional": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -10697,12 +10896,51 @@
"node": ">=8"
}
},
+ "node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "license": "ISC",
+ "optional": true
+ },
+ "node_modules/simple-concat": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
+ "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/simple-get": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz",
+ "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "decompress-response": "^4.2.0",
+ "once": "^1.3.1",
+ "simple-concat": "^1.0.0"
+ }
+ },
"node_modules/socket.io-client": {
"version": "4.8.3",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz",
"integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.4.1",
@@ -10884,6 +11122,32 @@
"url": "https://opencollective.com/webpack"
}
},
+ "node_modules/tar": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
+ "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
+ "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "minipass": "^5.0.0",
+ "minizlib": "^2.1.1",
+ "mkdirp": "^1.0.3",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/tar/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "license": "ISC",
+ "optional": true
+ },
"node_modules/text-encoding-utf-8": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz",
@@ -11255,7 +11519,6 @@
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
"integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==",
"license": "MIT",
- "peer": true,
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
@@ -11293,7 +11556,6 @@
"resolved": "https://registry.npmjs.org/valtio/-/valtio-1.13.2.tgz",
"integrity": "sha512-Qik0o+DSy741TmkqmRfjq+0xpZBXi/Y6+fXZLn0xNF1z/waFMbE3rkivv5Zcf9RrMUp6zswf2J7sbh2KBlba5A==",
"license": "MIT",
- "peer": true,
"dependencies": {
"derive-valtio": "0.1.0",
"proxy-compare": "2.6.0",
@@ -11335,7 +11597,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"@noble/curves": "1.9.1",
"@noble/hashes": "1.8.0",
@@ -11361,7 +11622,6 @@
"integrity": "sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.3",
@@ -11439,7 +11699,6 @@
"resolved": "https://registry.npmjs.org/wagmi/-/wagmi-2.19.5.tgz",
"integrity": "sha512-RQUfKMv6U+EcSNNGiPbdkDtJwtuFxZWLmvDiQmjjBgkuPulUwDJsKhi7gjynzJdsx2yDqhHCXkKsbbfbIsHfcQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@wagmi/connectors": "6.2.0",
"@wagmi/core": "2.22.1",
@@ -11554,6 +11813,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/wide-align": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
+ "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "string-width": "^1.0.2 || 2 || 3 || 4"
+ }
+ },
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -11592,7 +11861,6 @@
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=10.0.0"
},
@@ -11687,7 +11955,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/package.json b/package.json
index 3e0e8d0..a746977 100644
--- a/package.json
+++ b/package.json
@@ -19,7 +19,7 @@
"jszip": "^3.10.1",
"lucide-react": "^0.577.0",
"pdf-lib": "^1.17.1",
- "pdfjs-dist": "^5.5.207",
+ "pdfjs-dist": "^4.4.168",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-router-dom": "^7.13.1",
diff --git a/src/components/pdf/BatchPanel.jsx b/src/components/pdf/BatchPanel.jsx
new file mode 100644
index 0000000..a049bca
--- /dev/null
+++ b/src/components/pdf/BatchPanel.jsx
@@ -0,0 +1,169 @@
+import React from "react";
+import { X, FileText, Loader2, Download, CheckCircle2, PackageOpen } from "lucide-react";
+import { motion, AnimatePresence } from "framer-motion";
+import { Button } from "../ui/Button";
+import { formatFileSize } from "../../utils/formatters";
+
+/**
+ * Reusable batch mode panel.
+ *
+ * Props:
+ * batchFiles, addFiles, removeFile, clearFiles
+ * isProcessing, progress, error, done
+ * onRun — called when user clicks "Process & Download ZIP"
+ * runLabel — button label override (default "Process All & Download ZIP")
+ * previewUrl — optional: URL of first-file preview (iframe)
+ * accept — file input accept string (default "application/pdf")
+ */
+export function BatchPanel({
+ batchFiles,
+ addFiles,
+ removeFile,
+ isProcessing,
+ progress,
+ error,
+ done,
+ onRun,
+ runLabel = "Process All & Download ZIP",
+ previewUrl,
+ accept = "application/pdf",
+}) {
+ function handleDrop(e) {
+ e.preventDefault();
+ addFiles(Array.from(e.dataTransfer.files));
+ }
+
+ function handleInput(e) {
+ addFiles(Array.from(e.target.files));
+ e.target.value = "";
+ }
+
+ return (
+
+ {/* Drop zone */}
+
e.preventDefault()}
+ onDrop={handleDrop}
+ className="flex flex-col items-center justify-center w-full h-36 border-2 border-dashed border-white/10 rounded-2xl cursor-pointer hover:border-white/25 hover:bg-white/[0.02] transition-all"
+ >
+
+
+ Drop PDFs here or browse
+
+ Multiple files supported
+
+
+
+ {/* File list */}
+
+ {batchFiles.map((file, i) => (
+
+
+
+
+
{file.name}
+
{formatFileSize(file.size)}
+
+ {/* Only show preview badge for first file */}
+ {i === 0 && previewUrl && (
+
+ preview
+
+ )}
+
removeFile(i)}
+ disabled={isProcessing}
+ className="p-1.5 text-zinc-600 hover:text-red-400 hover:bg-white/5 rounded-lg transition-colors disabled:opacity-40"
+ >
+
+
+
+
+ ))}
+
+
+ {/* First-file preview */}
+ {previewUrl && (
+
+
+ Preview — first file only
+
+
+
+ )}
+
+ {/* Error */}
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* Progress */}
+
+ {isProcessing && (
+
+
+
+
+
+ Processing {progress.current} of {progress.total}…
+
+
+ {Math.round((progress.current / progress.total) * 100)}%
+
+
+
+
+
+
+
+ )}
+
+
+ {/* CTA */}
+ {batchFiles.length > 0 && (
+
+ {isProcessing ? (
+ <> Processing…>
+ ) : done ? (
+ <> Downloaded!>
+ ) : (
+ <> {runLabel} ({batchFiles.length} file{batchFiles.length > 1 ? "s" : ""})>
+ )}
+
+ )}
+
+ );
+}
diff --git a/src/components/ui/BatchToggle.jsx b/src/components/ui/BatchToggle.jsx
new file mode 100644
index 0000000..ed0d737
--- /dev/null
+++ b/src/components/ui/BatchToggle.jsx
@@ -0,0 +1,38 @@
+import React from "react";
+import { Layers, FileText } from "lucide-react";
+
+/**
+ * Small toggle that switches between single-file and batch mode.
+ */
+export function BatchToggle({ isBatchMode, onChange, disabled }) {
+ return (
+
+ onChange(false)}
+ disabled={disabled}
+ className={[
+ "flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all",
+ !isBatchMode
+ ? "bg-white text-black shadow"
+ : "text-zinc-500 hover:text-white",
+ ].join(" ")}
+ >
+
+ Single
+
+ onChange(true)}
+ disabled={disabled}
+ className={[
+ "flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all",
+ isBatchMode
+ ? "bg-white text-black shadow"
+ : "text-zinc-500 hover:text-white",
+ ].join(" ")}
+ >
+
+ Batch
+
+
+ );
+}
diff --git a/src/hooks/useBatchProcess.js b/src/hooks/useBatchProcess.js
new file mode 100644
index 0000000..581d73a
--- /dev/null
+++ b/src/hooks/useBatchProcess.js
@@ -0,0 +1,91 @@
+import { useState, useCallback } from "react";
+import JSZip from "jszip";
+
+/**
+ * Reusable batch processing hook.
+ *
+ * @param {Function} processFn - async (file, options) => Blob
+ * @param {Function} getOutputName - (originalName) => string
+ */
+export function useBatchProcess(processFn, getOutputName) {
+ const [isBatchMode, setIsBatchMode] = useState(false);
+ const [batchFiles, setBatchFiles] = useState([]);
+ const [isProcessing, setIsProcessing] = useState(false);
+ const [progress, setProgress] = useState({ current: 0, total: 0 });
+ const [error, setError] = useState(null);
+ const [done, setDone] = useState(false);
+
+ const addFiles = useCallback((incoming) => {
+ const pdfs = incoming.filter((f) => f.type === "application/pdf");
+ if (pdfs.length === 0) { setError("Please upload valid PDF files."); return; }
+ setError(null);
+ setDone(false);
+ setBatchFiles((prev) => {
+ // deduplicate by name+size
+ const existing = new Set(prev.map((f) => `${f.name}-${f.size}`));
+ const fresh = pdfs.filter((f) => !existing.has(`${f.name}-${f.size}`));
+ return [...prev, ...fresh];
+ });
+ }, []);
+
+ const removeFile = useCallback((index) => {
+ setBatchFiles((prev) => prev.filter((_, i) => i !== index));
+ setDone(false);
+ }, []);
+
+ const clearFiles = useCallback(() => {
+ setBatchFiles([]);
+ setDone(false);
+ setError(null);
+ setProgress({ current: 0, total: 0 });
+ }, []);
+
+ /**
+ * Process all files with the given options and download as ZIP.
+ * Processes one file at a time to avoid memory spikes.
+ */
+ const runBatch = useCallback(async (options = {}) => {
+ if (batchFiles.length === 0) return;
+ setIsProcessing(true);
+ setError(null);
+ setDone(false);
+ setProgress({ current: 0, total: batchFiles.length });
+
+ const zip = new JSZip();
+
+ try {
+ for (let i = 0; i < batchFiles.length; i++) {
+ const file = batchFiles[i];
+ setProgress({ current: i + 1, total: batchFiles.length });
+
+ // Give the browser and any WASM workers time to reset between files
+ await new Promise((r) => setTimeout(r, 150));
+
+ const blob = await processFn(file, options);
+ const outputName = getOutputName(file.name);
+ zip.file(outputName, blob);
+ }
+
+ const zipBlob = await zip.generateAsync({ type: "blob" });
+ const url = URL.createObjectURL(zipBlob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = `QuickPDF_Batch_${Date.now()}.zip`;
+ a.click();
+ URL.revokeObjectURL(url);
+ setDone(true);
+ } catch (err) {
+ console.error(err);
+ setError("One or more files failed to process. They may be encrypted or corrupted.");
+ } finally {
+ setIsProcessing(false);
+ }
+ }, [batchFiles, processFn, getOutputName]);
+
+ return {
+ isBatchMode, setIsBatchMode,
+ batchFiles, addFiles, removeFile, clearFiles,
+ isProcessing, progress, error, done,
+ runBatch,
+ };
+}
diff --git a/src/pages/Compress/Compress.jsx b/src/pages/Compress/Compress.jsx
index aa3a1a4..5ee067a 100644
--- a/src/pages/Compress/Compress.jsx
+++ b/src/pages/Compress/Compress.jsx
@@ -4,6 +4,9 @@ import { Minimize2, X, Download, Loader2, Zap, ShieldCheck, Gauge, Flame } from
import { motion, AnimatePresence } from "framer-motion";
import { Button } from "../../components/ui/Button";
import { UpgradeButton } from "../../components/ui/UpgradeButton";
+import { BatchToggle } from "../../components/ui/BatchToggle";
+import { BatchPanel } from "../../components/pdf/BatchPanel";
+import { useBatchProcess } from "../../hooks/useBatchProcess";
import { compressWithQuality } from "../../services/pdf.service";
import { Dropzone } from "../../components/pdf/Dropzone";
import { formatFileSize } from "../../utils/formatters";
@@ -19,6 +22,11 @@ export function Compress() {
const { isPremium, hasReachedGlobalLimit, incrementUsage, isWalletConnected } = useSubscription();
+ const batch = useBatchProcess(
+ (f, opts) => compressWithQuality(f, opts.quality),
+ (name) => `QuickPDF_Compressed_${name}`
+ );
+
const fileTooLarge = !isPremium && file && file.size > mbToBytes(FREE_LIMITS.compress.maxFileSizeMb);
const isLocked = hasReachedGlobalLimit || fileTooLarge;
const lockReason = hasReachedGlobalLimit ? "global" : "size";
@@ -77,9 +85,41 @@ export function Compress() {
)}
+
+
+
-
+ {batch.isBatchMode ? (
+
+
+
Compression Level (applied to all files)
+
+ {options.map((opt) => (
+
setLevel(opt.id)}
+ className={`flex flex-col items-center justify-center p-4 rounded-xl border transition-all ${level === opt.id ? "bg-white/10 border-white/30 text-white" : "bg-zinc-900/30 border-white/5 text-zinc-400 hover:bg-zinc-900/60 hover:border-white/15"}`}
+ >
+ {opt.icon}
+ {opt.label}
+ {opt.desc}
+
+ ))}
+
+
+
batch.runBatch({ quality: options.find(o => o.id === level).val })}
+ runLabel="Compress All & Download ZIP"
+ />
+
+ ) : (
+
{error && (
{error}
)}
@@ -149,7 +189,8 @@ export function Compress() {
)}
-
+
+ )} {/* end batch.isBatchMode conditional */}
);
}
\ No newline at end of file
diff --git a/src/pages/Grayscale/Grayscale.jsx b/src/pages/Grayscale/Grayscale.jsx
index 3437b24..41d3f20 100644
--- a/src/pages/Grayscale/Grayscale.jsx
+++ b/src/pages/Grayscale/Grayscale.jsx
@@ -4,6 +4,9 @@ import { Contrast, X, Download, Loader2, FileText } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
import { Button } from "../../components/ui/Button";
import { UpgradeButton } from "../../components/ui/UpgradeButton";
+import { BatchToggle } from "../../components/ui/BatchToggle";
+import { BatchPanel } from "../../components/pdf/BatchPanel";
+import { useBatchProcess } from "../../hooks/useBatchProcess";
import { convertToGrayscale } from "../../services/pdf.service";
import { Dropzone } from "../../components/pdf/Dropzone";
import { formatFileSize } from "../../utils/formatters";
@@ -19,6 +22,23 @@ export function Grayscale() {
const { isPremium, hasReachedGlobalLimit, incrementUsage, isWalletConnected } = useSubscription();
+ const [batchPreviewUrl, setBatchPreviewUrl] = useState(null);
+
+ const batch = useBatchProcess(
+ (f) => convertToGrayscale(f, () => {}),
+ (name) => `Grayscale_${name}`
+ );
+
+ const handleBatchFilesAdded = async (files) => {
+ batch.addFiles(files);
+ if (files.length > 0 && !batchPreviewUrl) {
+ try {
+ const blob = await convertToGrayscale(files[0], () => {});
+ setBatchPreviewUrl(URL.createObjectURL(blob));
+ } catch { /* preview is optional */ }
+ }
+ };
+
const fileTooLarge = !isPremium && file && file.size > mbToBytes(FREE_LIMITS.grayscale.maxFileSizeMb);
const isLocked = hasReachedGlobalLimit || fileTooLarge;
const lockReason = hasReachedGlobalLimit ? "global" : "size";
@@ -61,8 +81,27 @@ export function Grayscale() {
Strip colors from your document to save printing ink. Processed locally in your browser.
{!isPremium && Free tier: files up to {FREE_LIMITS.grayscale.maxFileSizeMb} MB }
+
+ { batch.setIsBatchMode(v); setBatchPreviewUrl(null); batch.clearFiles(); }} disabled={isProcessing || batch.isProcessing} />
+
+ {batch.isBatchMode ? (
+
+ { batch.removeFile(i); if (i === 0) setBatchPreviewUrl(null); }}
+ isProcessing={batch.isProcessing}
+ progress={batch.progress}
+ error={batch.error}
+ done={batch.done}
+ onRun={() => batch.runBatch()}
+ runLabel="Convert All to Grayscale & Download ZIP"
+ previewUrl={batchPreviewUrl}
+ />
+
+ ) : (
{error &&
{error}
}
@@ -125,6 +164,7 @@ export function Grayscale() {
)}
+ )} {/* end batch.isBatchMode conditional */}
);
}
diff --git a/src/pages/LockPdf/LockPdf.jsx b/src/pages/LockPdf/LockPdf.jsx
index fd6c267..b8265e9 100644
--- a/src/pages/LockPdf/LockPdf.jsx
+++ b/src/pages/LockPdf/LockPdf.jsx
@@ -7,6 +7,9 @@ import {
import { motion, AnimatePresence } from "framer-motion";
import { Button } from "../../components/ui/Button";
import { UpgradeButton } from "../../components/ui/UpgradeButton";
+import { BatchToggle } from "../../components/ui/BatchToggle";
+import { BatchPanel } from "../../components/pdf/BatchPanel";
+import { useBatchProcess } from "../../hooks/useBatchProcess";
import { Dropzone } from "../../components/pdf/Dropzone";
import { formatFileSize } from "../../utils/formatters";
import { lockPdf } from "../../services/pdf.service";
@@ -40,6 +43,11 @@ export function LockPdf() {
const { isPremium, isWalletConnected: isConnected, hasReachedGlobalLimit, incrementUsage } =
useSubscription();
+ const batch = useBatchProcess(
+ (f, opts) => lockPdf(f, opts.password),
+ (name) => `locked_${name}`
+ );
+
const LIMIT_MB = FREE_LIMITS.lockPdf.maxFileSizeMb;
const isOverSize = !isPremium && !!file && file.size > mbToBytes(LIMIT_MB);
const isLocked = !isPremium && (isOverSize || hasReachedGlobalLimit);
@@ -104,6 +112,9 @@ export function LockPdf() {
Password-protect your document entirely in the browser — nothing is uploaded to any server.
+
+
+
{/* global limit banner */}
@@ -120,12 +131,53 @@ export function LockPdf() {
{/* ── Drop zone ── */}
- {!file ? (
+ {!file && !batch.isBatchMode ? (
{ setFile(f[0]); setDone(false); }}
multiple={false}
text="Drop a PDF to lock it"
/>
+ ) : batch.isBatchMode ? (
+
+
The same password will be applied to all files.
+ {/* Password inputs reused */}
+
+
+ setPassword(e.target.value)}
+ placeholder="Password for all files"
+ className="w-full h-12 px-4 pr-12 bg-zinc-900/60 border border-white/10 rounded-xl text-sm text-white placeholder-zinc-600 focus:outline-none focus:border-white/30"
+ autoComplete="new-password" />
+ setShowPw(v => !v)}
+ className="absolute right-3 top-1/2 -translate-y-1/2 p-1 text-zinc-500 hover:text-white">
+ {showPw ? : }
+
+
+
+ setConfirm(e.target.value)}
+ placeholder="Confirm password"
+ className={`w-full h-12 px-4 pr-12 bg-zinc-900/60 border rounded-xl text-sm text-white placeholder-zinc-600 focus:outline-none transition-colors ${mismatch ? "border-red-500/50" : "border-white/10 focus:border-white/30"}`}
+ autoComplete="new-password" />
+ setShowConfirm(v => !v)}
+ className="absolute right-3 top-1/2 -translate-y-1/2 p-1 text-zinc-500 hover:text-white">
+ {showConfirm ? : }
+
+
+ {mismatch &&
Passwords don't match
}
+
+
{ if (password && password === confirm) batch.addFiles(files); }}
+ removeFile={batch.removeFile}
+ isProcessing={batch.isProcessing}
+ progress={batch.progress}
+ error={batch.error || (!password || mismatch ? "Set a matching password before adding files." : null)}
+ done={batch.done}
+ onRun={() => password && password === confirm && batch.runBatch({ password })}
+ runLabel="Lock All & Download ZIP"
+ />
+
) : (
diff --git a/src/pages/PageNumbers/PageNumbers.jsx b/src/pages/PageNumbers/PageNumbers.jsx
index 9e448d9..9da117d 100644
--- a/src/pages/PageNumbers/PageNumbers.jsx
+++ b/src/pages/PageNumbers/PageNumbers.jsx
@@ -4,6 +4,9 @@ import { Hash, Download, Loader2, CheckCircle2, AlertTriangle, AlignLeft, AlignC
import { motion, AnimatePresence } from "framer-motion";
import { Button } from "../../components/ui/Button";
import { UpgradeButton } from "../../components/ui/UpgradeButton";
+import { BatchToggle } from "../../components/ui/BatchToggle";
+import { BatchPanel } from "../../components/pdf/BatchPanel";
+import { useBatchProcess } from "../../hooks/useBatchProcess";
import { Dropzone } from "../../components/pdf/Dropzone";
import { formatFileSize } from "../../utils/formatters";
import { addPageNumbers } from "../../services/pdf.service";
@@ -29,6 +32,24 @@ export function PageNumbers() {
const [error, setError] = useState(null);
const { isPremium, isWalletConnected: isConnected, hasReachedGlobalLimit, incrementUsage } = useSubscription();
+
+ const [batchPreviewUrl, setBatchPreviewUrl] = useState(null);
+
+ const batch = useBatchProcess(
+ (f, opts) => addPageNumbers(f, opts),
+ (name) => `numbered_${name}`
+ );
+
+ const handleBatchFilesAdded = async (files) => {
+ batch.addFiles(files);
+ if (files.length > 0 && !batchPreviewUrl) {
+ try {
+ const blob = await addPageNumbers(files[0], { position, fontSize, prefix, startNumber });
+ setBatchPreviewUrl(URL.createObjectURL(blob));
+ } catch { /* preview is optional */ }
+ }
+ };
+
const LIMIT_MB = FREE_LIMITS.pageNumbers.maxFileSizeMb;
const isOverSizeLimit = !isPremium && !!file && file.size > mbToBytes(LIMIT_MB);
const isLocked = !isPremium && (isOverSizeLimit || hasReachedGlobalLimit);
@@ -76,6 +97,9 @@ export function PageNumbers() {
Auto-stamp sequential numbers on every page footer. Processed entirely in your browser.
+
+ { batch.setIsBatchMode(v); setBatchPreviewUrl(null); batch.clearFiles(); }} disabled={isProcessing || batch.isProcessing} />
+
{hasReachedGlobalLimit && !isPremium && (
@@ -90,8 +114,43 @@ export function PageNumbers() {
- {!file ? (
+ {!file && !batch.isBatchMode ? (
{ setFile(f[0]); setDone(false); }} multiple={false} text="Drop a PDF to number its pages" />
+ ) : batch.isBatchMode ? (
+
+
Settings below will be applied to all files.
+
+
+
Position
+
+ {POSITIONS.map(({ value, label, Icon }) => (
+ setPosition(value)}
+ className={`flex-1 flex flex-col items-center gap-1 py-2 rounded-xl border text-xs font-medium transition-all ${position === value ? "border-white/40 bg-white/8 text-white" : "border-white/[0.06] text-zinc-500 hover:text-white"}`}>
+ {label}
+
+ ))}
+
+
+
+ Start #
+ setStartNumber(Math.max(0, parseInt(e.target.value) || 0))}
+ className="w-full h-10 px-3 bg-zinc-900/60 border border-white/10 rounded-xl text-sm text-white focus:outline-none focus:border-white/30" />
+
+
+
{ batch.removeFile(i); if (i === 0) setBatchPreviewUrl(null); }}
+ isProcessing={batch.isProcessing}
+ progress={batch.progress}
+ error={batch.error}
+ done={batch.done}
+ onRun={() => batch.runBatch({ position, fontSize, prefix, startNumber })}
+ runLabel="Add Page Numbers to All & Download ZIP"
+ previewUrl={batchPreviewUrl}
+ />
+
) : (
diff --git a/src/pages/Rotate/Rotate.jsx b/src/pages/Rotate/Rotate.jsx
index 14932e6..6463c07 100644
--- a/src/pages/Rotate/Rotate.jsx
+++ b/src/pages/Rotate/Rotate.jsx
@@ -4,6 +4,9 @@ import { RotateCw, RotateCcw, Download, Loader2, RefreshCw, AlertTriangle, Check
import { motion, AnimatePresence } from "framer-motion";
import { Button } from "../../components/ui/Button";
import { UpgradeButton } from "../../components/ui/UpgradeButton";
+import { BatchToggle } from "../../components/ui/BatchToggle";
+import { BatchPanel } from "../../components/pdf/BatchPanel";
+import { useBatchProcess } from "../../hooks/useBatchProcess";
import { rotatePdfPerPage } from "../../services/pdf.service";
import { Dropzone } from "../../components/pdf/Dropzone";
import { formatFileSize } from "../../utils/formatters";
@@ -134,6 +137,20 @@ export function Rotate() {
const sentinelRef = useRef(null);
const { isPremium, isWalletConnected: isConnected, hasReachedGlobalLimit, incrementUsage } = useSubscription();
+
+ // Batch: rotate all pages of each file by a fixed angle
+ const [batchAngle, setBatchAngle] = useState(90);
+ const batch = useBatchProcess(
+ async (f, opts) => {
+ const { PDFDocument, degrees } = await import("pdf-lib");
+ const buf = await f.arrayBuffer();
+ const doc = await PDFDocument.load(buf);
+ doc.getPages().forEach(p => p.setRotation(degrees(p.getRotation().angle + opts.angle)));
+ return new Blob([await doc.save()], { type: "application/pdf" });
+ },
+ (name) => `rotated_${name}`
+ );
+
const ROTATE_LIMIT_MB = FREE_LIMITS.rotate.maxFileSizeMb;
const isOverSizeLimit = !isPremium && !!file && file.size > mbToBytes(ROTATE_LIMIT_MB);
const isLocked = !isPremium && (isOverSizeLimit || hasReachedGlobalLimit);
@@ -211,6 +228,9 @@ export function Rotate() {
Rotate individual pages or the whole document. Processed entirely in your browser.
+
+
+
{hasReachedGlobalLimit && !isPremium && (
@@ -225,7 +245,34 @@ export function Rotate() {
- {!file ? (
+ {batch.isBatchMode ? (
+
+
+
+ Rotation angle (applied to all pages of all files)
+
+
+ {[-90, 90, 180].map((angle) => (
+ setBatchAngle(angle)}
+ className={`flex-1 py-3 rounded-xl border text-sm font-semibold transition-all ${batchAngle === angle ? "border-white/40 bg-white/8 text-white" : "border-white/[0.06] text-zinc-500 hover:text-white"}`}>
+ {angle > 0 ? `+${angle}°` : `${angle}°`}
+
+ ))}
+
+
+
batch.runBatch({ angle: batchAngle })}
+ runLabel="Rotate All & Download ZIP"
+ />
+
+ ) : !file ? (
setFile(f[0])} multiple={false} text="Drop PDF to rotate" />
) : (
diff --git a/src/pages/Watermark/Watermark.jsx b/src/pages/Watermark/Watermark.jsx
index cad75c6..9963e61 100644
--- a/src/pages/Watermark/Watermark.jsx
+++ b/src/pages/Watermark/Watermark.jsx
@@ -3,6 +3,9 @@ import { useFileStore } from "../../hooks/useFileStore";
import { Stamp, X, Download, Loader2 } from "lucide-react";
import { Button } from "../../components/ui/Button";
import { UpgradeButton } from "../../components/ui/UpgradeButton";
+import { BatchToggle } from "../../components/ui/BatchToggle";
+import { BatchPanel } from "../../components/pdf/BatchPanel";
+import { useBatchProcess } from "../../hooks/useBatchProcess";
import { addWatermark } from "../../services/pdf.service";
import { Dropzone } from "../../components/pdf/Dropzone";
import { formatFileSize } from "../../utils/formatters";
@@ -15,6 +18,7 @@ export function Watermark() {
const [isProcessing, setIsProcessing] = useState(false);
const [error, setError] = useState(null);
const [previewUrl, setPreviewUrl] = useState(null);
+ const [batchPreviewUrl, setBatchPreviewUrl] = useState(null);
const {
isPremium,
@@ -23,6 +27,11 @@ export function Watermark() {
isWalletConnected,
} = useSubscription();
+ const batch = useBatchProcess(
+ (f, opts) => addWatermark(f, opts.watermarkText),
+ (name) => `QuickPDF_Watermarked_${name}`
+ );
+
const fileTooLarge =
!isPremium &&
file &&
@@ -74,29 +83,64 @@ export function Watermark() {
}
};
+ const handleBatchFilesAdded = async (files) => {
+ batch.addFiles(files);
+ // Generate preview for first file only
+ if (files.length > 0 && !batchPreviewUrl) {
+ try {
+ const blob = await addWatermark(files[0], watermarkText);
+ setBatchPreviewUrl(URL.createObjectURL(blob));
+ } catch { /* silent — preview is optional */ }
+ }
+ };
+
return (
-
-
- Add Watermark
-
-
+
Add Watermark
Stamp text across your document securely in your browser.
{!isPremium && (
- Free tier: files up to{" "}
- {FREE_LIMITS.watermark.maxFileSizeMb} MB
+ Free tier: files up to {FREE_LIMITS.watermark.maxFileSizeMb} MB
)}
+
+ { batch.setIsBatchMode(v); setBatchPreviewUrl(null); batch.clearFiles(); }} disabled={isProcessing || batch.isProcessing} />
+
+ {batch.isBatchMode ? (
+
+
+ Watermark Text (applied to all files)
+ setWatermarkText(e.target.value)}
+ placeholder="e.g., CONFIDENTIAL"
+ className="w-full h-11 px-4 bg-black border border-white/10 text-white rounded-lg focus:ring-2 focus:ring-white/20 outline-none transition-all placeholder:text-zinc-600 uppercase"
+ />
+
+
{ batch.removeFile(i); if (i === 0) setBatchPreviewUrl(null); }}
+ isProcessing={batch.isProcessing}
+ progress={batch.progress}
+ error={batch.error}
+ done={batch.done}
+ onRun={() => batch.runBatch({ watermarkText })}
+ runLabel="Watermark All & Download ZIP"
+ previewUrl={batchPreviewUrl}
+ />
+
+ ) : (
{error && (
@@ -184,8 +228,9 @@ export function Watermark() {
)}
+ )} {/* end batch.isBatchMode conditional */}
- {previewUrl && (
+ {!batch.isBatchMode && previewUrl && (
Preview
From ce5902ed3340c0c52cc84a2593cc167abfff0042 Mon Sep 17 00:00:00 2001
From: Ayush Lad
Date: Mon, 20 Apr 2026 00:46:07 +0530
Subject: [PATCH 2/3] =?UTF-8?q?fix:=20address=20copilot=20review=20?=
=?UTF-8?q?=E2=80=94=20subscription=20enforcement,=20URL=20cleanup,=20serv?=
=?UTF-8?q?ice=20reuse,=20env=20handling?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.env.example | 2 +
.gitignore | 5 +
package.json | 2 +-
src/components/pdf/BatchPanel.jsx | 27 +--
src/hooks/useBatchProcess.js | 43 +++--
src/pages/Compress/Compress.jsx | 14 +-
src/pages/Grayscale/Grayscale.jsx | 25 ++-
src/pages/LockPdf/LockPdf.jsx | 14 +-
src/pages/PageNumbers/PageNumbers.jsx | 20 +-
src/pages/Rotate/Rotate.jsx | 21 +-
src/pages/Watermark/Watermark.jsx | 267 ++++++++++----------------
11 files changed, 222 insertions(+), 218 deletions(-)
create mode 100644 .env.example
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..c55c6ac
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,2 @@
+# Get a free projectId at https://cloud.walletconnect.com
+VITE_WALLETCONNECT_PROJECT_ID=your_project_id_here
diff --git a/.gitignore b/.gitignore
index a547bf3..8dff0e3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,11 @@ dist
dist-ssr
*.local
+# Environment variables — never commit real secrets
+.env
+.env.*
+!.env.example
+
# Editor directories and files
.vscode/*
!.vscode/extensions.json
diff --git a/package.json b/package.json
index a746977..2b9c534 100644
--- a/package.json
+++ b/package.json
@@ -19,7 +19,7 @@
"jszip": "^3.10.1",
"lucide-react": "^0.577.0",
"pdf-lib": "^1.17.1",
- "pdfjs-dist": "^4.4.168",
+ "pdfjs-dist": "4.4.168",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-router-dom": "^7.13.1",
diff --git a/src/components/pdf/BatchPanel.jsx b/src/components/pdf/BatchPanel.jsx
index a049bca..cac33e1 100644
--- a/src/components/pdf/BatchPanel.jsx
+++ b/src/components/pdf/BatchPanel.jsx
@@ -4,17 +4,6 @@ import { motion, AnimatePresence } from "framer-motion";
import { Button } from "../ui/Button";
import { formatFileSize } from "../../utils/formatters";
-/**
- * Reusable batch mode panel.
- *
- * Props:
- * batchFiles, addFiles, removeFile, clearFiles
- * isProcessing, progress, error, done
- * onRun — called when user clicks "Process & Download ZIP"
- * runLabel — button label override (default "Process All & Download ZIP")
- * previewUrl — optional: URL of first-file preview (iframe)
- * accept — file input accept string (default "application/pdf")
- */
export function BatchPanel({
batchFiles,
addFiles,
@@ -24,12 +13,14 @@ export function BatchPanel({
error,
done,
onRun,
+ runDisabled = false, // extra disable flag (e.g. password not set)
runLabel = "Process All & Download ZIP",
previewUrl,
accept = "application/pdf",
}) {
function handleDrop(e) {
e.preventDefault();
+ if (isProcessing) return; // ignore drops mid-run
addFiles(Array.from(e.dataTransfer.files));
}
@@ -38,13 +29,18 @@ export function BatchPanel({
e.target.value = "";
}
+ const canRun = batchFiles.length > 0 && !isProcessing && !runDisabled;
+
return (
{/* Drop zone */}
e.preventDefault()}
onDrop={handleDrop}
- className="flex flex-col items-center justify-center w-full h-36 border-2 border-dashed border-white/10 rounded-2xl cursor-pointer hover:border-white/25 hover:bg-white/[0.02] transition-all"
+ className={`flex flex-col items-center justify-center w-full h-36 border-2 border-dashed rounded-2xl transition-all
+ ${isProcessing
+ ? "border-white/5 opacity-40 cursor-not-allowed"
+ : "border-white/10 cursor-pointer hover:border-white/25 hover:bg-white/[0.02]"}`}
>
@@ -77,7 +73,6 @@ export function BatchPanel({
{file.name}
{formatFileSize(file.size)}
- {/* Only show preview badge for first file */}
{i === 0 && previewUrl && (
preview
@@ -150,11 +145,7 @@ export function BatchPanel({
{/* CTA */}
{batchFiles.length > 0 && (
-
+
{isProcessing ? (
<> Processing…>
) : done ? (
diff --git a/src/hooks/useBatchProcess.js b/src/hooks/useBatchProcess.js
index 581d73a..606ff27 100644
--- a/src/hooks/useBatchProcess.js
+++ b/src/hooks/useBatchProcess.js
@@ -4,10 +4,13 @@ import JSZip from "jszip";
/**
* Reusable batch processing hook.
*
- * @param {Function} processFn - async (file, options) => Blob
- * @param {Function} getOutputName - (originalName) => string
+ * @param {Function} processFn - async (file, options) => Blob
+ * @param {Function} getOutputName - (originalName) => string
+ * @param {Object} guards - optional { canProcess(file) => bool, onAfterEach() => Promise }
+ * canProcess — called before each file; return false to skip (e.g. size/global limit check)
+ * onAfterEach — called after each successful file (e.g. incrementUsage)
*/
-export function useBatchProcess(processFn, getOutputName) {
+export function useBatchProcess(processFn, getOutputName, guards = {}) {
const [isBatchMode, setIsBatchMode] = useState(false);
const [batchFiles, setBatchFiles] = useState([]);
const [isProcessing, setIsProcessing] = useState(false);
@@ -16,17 +19,17 @@ export function useBatchProcess(processFn, getOutputName) {
const [done, setDone] = useState(false);
const addFiles = useCallback((incoming) => {
+ if (isProcessing) return; // ignore drops mid-run
const pdfs = incoming.filter((f) => f.type === "application/pdf");
if (pdfs.length === 0) { setError("Please upload valid PDF files."); return; }
setError(null);
setDone(false);
setBatchFiles((prev) => {
- // deduplicate by name+size
const existing = new Set(prev.map((f) => `${f.name}-${f.size}`));
const fresh = pdfs.filter((f) => !existing.has(`${f.name}-${f.size}`));
return [...prev, ...fresh];
});
- }, []);
+ }, [isProcessing]);
const removeFile = useCallback((index) => {
setBatchFiles((prev) => prev.filter((_, i) => i !== index));
@@ -42,10 +45,17 @@ export function useBatchProcess(processFn, getOutputName) {
/**
* Process all files with the given options and download as ZIP.
- * Processes one file at a time to avoid memory spikes.
+ * Respects canProcess guard and calls onAfterEach per file.
*/
const runBatch = useCallback(async (options = {}) => {
if (batchFiles.length === 0) return;
+
+ // Check global guard before starting
+ if (guards.canProcess && !guards.canProcess(batchFiles[0])) {
+ setError("You have reached your free-tier limit. Upgrade to continue.");
+ return;
+ }
+
setIsProcessing(true);
setError(null);
setDone(false);
@@ -58,12 +68,20 @@ export function useBatchProcess(processFn, getOutputName) {
const file = batchFiles[i];
setProgress({ current: i + 1, total: batchFiles.length });
- // Give the browser and any WASM workers time to reset between files
+ // Per-file guard (size limit etc.)
+ if (guards.canProcess && !guards.canProcess(file)) {
+ setError(`"${file.name}" exceeds the free-tier size limit and was skipped.`);
+ continue;
+ }
+
+ // Yield to event loop — keeps UI responsive and lets WASM workers reset
await new Promise((r) => setTimeout(r, 150));
const blob = await processFn(file, options);
- const outputName = getOutputName(file.name);
- zip.file(outputName, blob);
+ zip.file(getOutputName(file.name), blob);
+
+ // Increment usage per file
+ if (guards.onAfterEach) await guards.onAfterEach();
}
const zipBlob = await zip.generateAsync({ type: "blob" });
@@ -71,8 +89,11 @@ export function useBatchProcess(processFn, getOutputName) {
const a = document.createElement("a");
a.href = url;
a.download = `QuickPDF_Batch_${Date.now()}.zip`;
+ document.body.appendChild(a);
a.click();
- URL.revokeObjectURL(url);
+ document.body.removeChild(a);
+ // Revoke after next tick so the browser has time to start the download
+ setTimeout(() => URL.revokeObjectURL(url), 100);
setDone(true);
} catch (err) {
console.error(err);
@@ -80,7 +101,7 @@ export function useBatchProcess(processFn, getOutputName) {
} finally {
setIsProcessing(false);
}
- }, [batchFiles, processFn, getOutputName]);
+ }, [batchFiles, processFn, getOutputName, guards]);
return {
isBatchMode, setIsBatchMode,
diff --git a/src/pages/Compress/Compress.jsx b/src/pages/Compress/Compress.jsx
index 5ee067a..f217165 100644
--- a/src/pages/Compress/Compress.jsx
+++ b/src/pages/Compress/Compress.jsx
@@ -24,7 +24,11 @@ export function Compress() {
const batch = useBatchProcess(
(f, opts) => compressWithQuality(f, opts.quality),
- (name) => `QuickPDF_Compressed_${name}`
+ (name) => `QuickPDF_Compressed_${name}`,
+ {
+ canProcess: (f) => !hasReachedGlobalLimit && (isPremium || f.size <= mbToBytes(FREE_LIMITS.compress.maxFileSizeMb)),
+ onAfterEach: incrementUsage,
+ }
);
const fileTooLarge = !isPremium && file && file.size > mbToBytes(FREE_LIMITS.compress.maxFileSizeMb);
@@ -86,12 +90,17 @@ export function Compress() {
)}
-
+ { batch.setIsBatchMode(v); batch.clearFiles(); }} disabled={isProcessing || batch.isProcessing} />
{batch.isBatchMode ? (
+ {isLocked && (
+
+
+
+ )}
Compression Level (applied to all files)
@@ -116,6 +125,7 @@ export function Compress() {
done={batch.done}
onRun={() => batch.runBatch({ quality: options.find(o => o.id === level).val })}
runLabel="Compress All & Download ZIP"
+ runDisabled={isLocked}
/>
) : (
diff --git a/src/pages/Grayscale/Grayscale.jsx b/src/pages/Grayscale/Grayscale.jsx
index 41d3f20..2f5fac5 100644
--- a/src/pages/Grayscale/Grayscale.jsx
+++ b/src/pages/Grayscale/Grayscale.jsx
@@ -1,4 +1,4 @@
-import React, { useState } from "react";
+import React, { useState, useEffect, useRef } from "react";
import { useFileStore } from "../../hooks/useFileStore";
import { Contrast, X, Download, Loader2, FileText } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
@@ -23,17 +23,28 @@ export function Grayscale() {
const { isPremium, hasReachedGlobalLimit, incrementUsage, isWalletConnected } = useSubscription();
const [batchPreviewUrl, setBatchPreviewUrl] = useState(null);
+ const batchPreviewRef = useRef(null);
const batch = useBatchProcess(
(f) => convertToGrayscale(f, () => {}),
- (name) => `Grayscale_${name}`
+ (name) => `Grayscale_${name}`,
+ {
+ canProcess: (f) => !hasReachedGlobalLimit && (isPremium || f.size <= mbToBytes(FREE_LIMITS.grayscale.maxFileSizeMb)),
+ onAfterEach: incrementUsage,
+ }
);
+ useEffect(() => { batchPreviewRef.current = batchPreviewUrl; }, [batchPreviewUrl]);
+ useEffect(() => { return () => { if (batchPreviewRef.current) URL.revokeObjectURL(batchPreviewRef.current); }; }, []);
+
+ const clearBatchPreview = () => { if (batchPreviewRef.current) URL.revokeObjectURL(batchPreviewRef.current); setBatchPreviewUrl(null); };
+
const handleBatchFilesAdded = async (files) => {
batch.addFiles(files);
if (files.length > 0 && !batchPreviewUrl) {
try {
const blob = await convertToGrayscale(files[0], () => {});
+ clearBatchPreview();
setBatchPreviewUrl(URL.createObjectURL(blob));
} catch { /* preview is optional */ }
}
@@ -82,20 +93,26 @@ export function Grayscale() {
{!isPremium &&
Free tier: files up to {FREE_LIMITS.grayscale.maxFileSizeMb} MB }
- { batch.setIsBatchMode(v); setBatchPreviewUrl(null); batch.clearFiles(); }} disabled={isProcessing || batch.isProcessing} />
+ { batch.setIsBatchMode(v); clearBatchPreview(); batch.clearFiles(); }} disabled={isProcessing || batch.isProcessing} />
{batch.isBatchMode ? (
+ {isLocked && (
+
+
+
+ )}
{ batch.removeFile(i); if (i === 0) setBatchPreviewUrl(null); }}
+ removeFile={(i) => { batch.removeFile(i); if (i === 0) clearBatchPreview(); }}
isProcessing={batch.isProcessing}
progress={batch.progress}
error={batch.error}
done={batch.done}
+ runDisabled={isLocked}
onRun={() => batch.runBatch()}
runLabel="Convert All to Grayscale & Download ZIP"
previewUrl={batchPreviewUrl}
diff --git a/src/pages/LockPdf/LockPdf.jsx b/src/pages/LockPdf/LockPdf.jsx
index b8265e9..0d390be 100644
--- a/src/pages/LockPdf/LockPdf.jsx
+++ b/src/pages/LockPdf/LockPdf.jsx
@@ -45,7 +45,11 @@ export function LockPdf() {
const batch = useBatchProcess(
(f, opts) => lockPdf(f, opts.password),
- (name) => `locked_${name}`
+ (name) => `locked_${name}`,
+ {
+ canProcess: (f) => !hasReachedGlobalLimit && (isPremium || f.size <= mbToBytes(LIMIT_MB)),
+ onAfterEach: incrementUsage,
+ }
);
const LIMIT_MB = FREE_LIMITS.lockPdf.maxFileSizeMb;
@@ -139,6 +143,11 @@ export function LockPdf() {
/>
) : batch.isBatchMode ? (
+ {isLocked && (
+
+
+
+ )}
The same password will be applied to all files.
{/* Password inputs reused */}
@@ -174,7 +183,8 @@ export function LockPdf() {
progress={batch.progress}
error={batch.error || (!password || mismatch ? "Set a matching password before adding files." : null)}
done={batch.done}
- onRun={() => password && password === confirm && batch.runBatch({ password })}
+ runDisabled={!password || mismatch || isLocked}
+ onRun={() => batch.runBatch({ password })}
runLabel="Lock All & Download ZIP"
/>
diff --git a/src/pages/PageNumbers/PageNumbers.jsx b/src/pages/PageNumbers/PageNumbers.jsx
index 9da117d..4c6bea1 100644
--- a/src/pages/PageNumbers/PageNumbers.jsx
+++ b/src/pages/PageNumbers/PageNumbers.jsx
@@ -1,4 +1,4 @@
-import React, { useState } from "react";
+import React, { useState, useEffect, useRef } from "react";
import { useFileStore } from "../../hooks/useFileStore";
import { Hash, Download, Loader2, CheckCircle2, AlertTriangle, AlignLeft, AlignCenter, AlignRight } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
@@ -34,17 +34,28 @@ export function PageNumbers() {
const { isPremium, isWalletConnected: isConnected, hasReachedGlobalLimit, incrementUsage } = useSubscription();
const [batchPreviewUrl, setBatchPreviewUrl] = useState(null);
+ const batchPreviewRef = useRef(null);
const batch = useBatchProcess(
(f, opts) => addPageNumbers(f, opts),
- (name) => `numbered_${name}`
+ (name) => `numbered_${name}`,
+ {
+ canProcess: (f) => !hasReachedGlobalLimit && (isPremium || f.size <= mbToBytes(LIMIT_MB)),
+ onAfterEach: incrementUsage,
+ }
);
+ useEffect(() => { batchPreviewRef.current = batchPreviewUrl; }, [batchPreviewUrl]);
+ useEffect(() => { return () => { if (batchPreviewRef.current) URL.revokeObjectURL(batchPreviewRef.current); }; }, []);
+
+ const clearBatchPreview = () => { if (batchPreviewRef.current) URL.revokeObjectURL(batchPreviewRef.current); setBatchPreviewUrl(null); };
+
const handleBatchFilesAdded = async (files) => {
batch.addFiles(files);
if (files.length > 0 && !batchPreviewUrl) {
try {
const blob = await addPageNumbers(files[0], { position, fontSize, prefix, startNumber });
+ clearBatchPreview();
setBatchPreviewUrl(URL.createObjectURL(blob));
} catch { /* preview is optional */ }
}
@@ -98,7 +109,7 @@ export function PageNumbers() {
Auto-stamp sequential numbers on every page footer. Processed entirely in your browser.
- { batch.setIsBatchMode(v); setBatchPreviewUrl(null); batch.clearFiles(); }} disabled={isProcessing || batch.isProcessing} />
+ { batch.setIsBatchMode(v); clearBatchPreview(); batch.clearFiles(); }} disabled={isProcessing || batch.isProcessing} />
@@ -141,11 +152,12 @@ export function PageNumbers() {
{ batch.removeFile(i); if (i === 0) setBatchPreviewUrl(null); }}
+ removeFile={(i) => { batch.removeFile(i); if (i === 0) clearBatchPreview(); }}
isProcessing={batch.isProcessing}
progress={batch.progress}
error={batch.error}
done={batch.done}
+ runDisabled={isLocked}
onRun={() => batch.runBatch({ position, fontSize, prefix, startNumber })}
runLabel="Add Page Numbers to All & Download ZIP"
previewUrl={batchPreviewUrl}
diff --git a/src/pages/Rotate/Rotate.jsx b/src/pages/Rotate/Rotate.jsx
index 6463c07..02e1871 100644
--- a/src/pages/Rotate/Rotate.jsx
+++ b/src/pages/Rotate/Rotate.jsx
@@ -7,7 +7,7 @@ import { UpgradeButton } from "../../components/ui/UpgradeButton";
import { BatchToggle } from "../../components/ui/BatchToggle";
import { BatchPanel } from "../../components/pdf/BatchPanel";
import { useBatchProcess } from "../../hooks/useBatchProcess";
-import { rotatePdfPerPage } from "../../services/pdf.service";
+import { rotatePdfPerPage, rotatePdf } from "../../services/pdf.service";
import { Dropzone } from "../../components/pdf/Dropzone";
import { formatFileSize } from "../../utils/formatters";
import { useSubscription } from "../../hooks/useSubscription";
@@ -138,17 +138,14 @@ export function Rotate() {
const { isPremium, isWalletConnected: isConnected, hasReachedGlobalLimit, incrementUsage } = useSubscription();
- // Batch: rotate all pages of each file by a fixed angle
const [batchAngle, setBatchAngle] = useState(90);
const batch = useBatchProcess(
- async (f, opts) => {
- const { PDFDocument, degrees } = await import("pdf-lib");
- const buf = await f.arrayBuffer();
- const doc = await PDFDocument.load(buf);
- doc.getPages().forEach(p => p.setRotation(degrees(p.getRotation().angle + opts.angle)));
- return new Blob([await doc.save()], { type: "application/pdf" });
- },
- (name) => `rotated_${name}`
+ (f, opts) => rotatePdf(f, opts.angle),
+ (name) => `rotated_${name}`,
+ {
+ canProcess: (f) => !hasReachedGlobalLimit && (isPremium || f.size <= mbToBytes(ROTATE_LIMIT_MB)),
+ onAfterEach: incrementUsage,
+ }
);
const ROTATE_LIMIT_MB = FREE_LIMITS.rotate.maxFileSizeMb;
@@ -247,6 +244,9 @@ export function Rotate() {
{batch.isBatchMode ? (
+ {isLocked && (
+
+ )}
Rotation angle (applied to all pages of all files)
@@ -270,6 +270,7 @@ export function Rotate() {
done={batch.done}
onRun={() => batch.runBatch({ angle: batchAngle })}
runLabel="Rotate All & Download ZIP"
+ runDisabled={isLocked}
/>
) : !file ? (
diff --git a/src/pages/Watermark/Watermark.jsx b/src/pages/Watermark/Watermark.jsx
index 9963e61..7bec02c 100644
--- a/src/pages/Watermark/Watermark.jsx
+++ b/src/pages/Watermark/Watermark.jsx
@@ -1,4 +1,4 @@
-import React, { useState } from "react";
+import React, { useState, useEffect, useRef } from "react";
import { useFileStore } from "../../hooks/useFileStore";
import { Stamp, X, Download, Loader2 } from "lucide-react";
import { Button } from "../../components/ui/Button";
@@ -19,83 +19,85 @@ export function Watermark() {
const [error, setError] = useState(null);
const [previewUrl, setPreviewUrl] = useState(null);
const [batchPreviewUrl, setBatchPreviewUrl] = useState(null);
+ const batchPreviewRef = useRef(null);
- const {
- isPremium,
- hasReachedGlobalLimit,
- incrementUsage,
- isWalletConnected,
- } = useSubscription();
+ const { isPremium, hasReachedGlobalLimit, incrementUsage, isWalletConnected } = useSubscription();
+
+ const fileTooLarge = !isPremium && file && file.size > mbToBytes(FREE_LIMITS.watermark.maxFileSizeMb);
+ const isLocked = hasReachedGlobalLimit || fileTooLarge;
+ const lockReason = hasReachedGlobalLimit ? "global" : "size";
+ const lockLabel = fileTooLarge ? `${FREE_LIMITS.watermark.maxFileSizeMb} MB` : undefined;
const batch = useBatchProcess(
(f, opts) => addWatermark(f, opts.watermarkText),
- (name) => `QuickPDF_Watermarked_${name}`
+ (name) => `QuickPDF_Watermarked_${name}`,
+ {
+ canProcess: (f) => !hasReachedGlobalLimit && (isPremium || f.size <= mbToBytes(FREE_LIMITS.watermark.maxFileSizeMb)),
+ onAfterEach: incrementUsage,
+ }
);
- const fileTooLarge =
- !isPremium &&
- file &&
- file.size > mbToBytes(FREE_LIMITS.watermark.maxFileSizeMb);
-
- const isLocked = hasReachedGlobalLimit || fileTooLarge;
- const lockReason = hasReachedGlobalLimit ? "global" : "size";
- const lockLabel = fileTooLarge
- ? `${FREE_LIMITS.watermark.maxFileSizeMb} MB`
- : undefined;
+ // Revoke batch preview URL on change or unmount
+ useEffect(() => {
+ batchPreviewRef.current = batchPreviewUrl;
+ }, [batchPreviewUrl]);
+
+ useEffect(() => {
+ return () => {
+ if (batchPreviewRef.current) URL.revokeObjectURL(batchPreviewRef.current);
+ if (previewUrl) URL.revokeObjectURL(previewUrl);
+ };
+ }, []); // eslint-disable-line
+
+ const clearBatchPreview = () => {
+ if (batchPreviewRef.current) URL.revokeObjectURL(batchPreviewRef.current);
+ setBatchPreviewUrl(null);
+ };
const handleFileSelected = (selectedFiles) => {
const selectedFile = selectedFiles[0];
- if (!selectedFile) return;
-
- if (selectedFile.type !== "application/pdf") {
- setError("Please upload a valid PDF file.");
- return;
+ if (!selectedFile || selectedFile.type !== "application/pdf") {
+ setError("Please upload a valid PDF file."); return;
}
-
- setError(null);
- setFile(selectedFile);
- setPreviewUrl(null);
+ setError(null); setFile(selectedFile); setPreviewUrl(null);
};
- const clearFile = () => {
- setFile(null);
- setError(null);
- setPreviewUrl(null);
- };
+ const clearFile = () => { setFile(null); setError(null); setPreviewUrl(null); };
const handleProcess = async () => {
if (!file || !watermarkText.trim()) return;
-
try {
- setIsProcessing(true);
- setError(null);
-
- const watermarkedBlob = await addWatermark(file, watermarkText);
- const url = URL.createObjectURL(watermarkedBlob);
-
- setPreviewUrl(url);
+ setIsProcessing(true); setError(null);
+ const blob = await addWatermark(file, watermarkText);
+ setPreviewUrl(URL.createObjectURL(blob));
await incrementUsage();
} catch {
setError("Could not read the PDF file. It might be corrupted or encrypted.");
setFile(null);
- } finally {
- setIsProcessing(false);
- }
+ } finally { setIsProcessing(false); }
};
const handleBatchFilesAdded = async (files) => {
batch.addFiles(files);
- // Generate preview for first file only
if (files.length > 0 && !batchPreviewUrl) {
try {
const blob = await addWatermark(files[0], watermarkText);
+ clearBatchPreview();
setBatchPreviewUrl(URL.createObjectURL(blob));
- } catch { /* silent — preview is optional */ }
+ } catch { /* preview is optional */ }
}
};
+ const enterBatchMode = (v) => {
+ batch.setIsBatchMode(v);
+ clearBatchPreview();
+ batch.clearFiles();
+ // Clear single-file layout state when entering batch
+ if (v) { setPreviewUrl(null); setFile(null); }
+ };
+
return (
-
+
@@ -103,160 +105,93 @@ export function Watermark() {
Add Watermark
Stamp text across your document securely in your browser.
- {!isPremium && (
-
- Free tier: files up to {FREE_LIMITS.watermark.maxFileSizeMb} MB
-
- )}
+ {!isPremium && Free tier: files up to {FREE_LIMITS.watermark.maxFileSizeMb} MB }
- { batch.setIsBatchMode(v); setBatchPreviewUrl(null); batch.clearFiles(); }} disabled={isProcessing || batch.isProcessing} />
+
-
+
{batch.isBatchMode ? (
) : (
-
- {error && (
-
- {error}
-
- )}
-
- {!file ? (
-
- ) : (
-
-
-
-
- {file.name}
-
-
-
- {formatFileSize(file.size)}
- {fileTooLarge && (
-
- (exceeds free limit)
+
+ {error &&
{error}
}
+ {!file ? (
+
+ ) : (
+
+
+
+ {file.name}
+
+ {formatFileSize(file.size)}
+ {fileTooLarge && (exceeds free limit) }
- )}
-
-
-
-
-
-
-
-
-
-
- Watermark Text
-
-
- setWatermarkText(e.target.value)}
- placeholder="e.g., CONFIDENTIAL"
- className="w-full h-11 px-4 bg-black border border-white/10 text-white rounded-lg focus:ring-2 focus:ring-white/20 outline-none transition-all placeholder:text-zinc-600 uppercase"
- />
-
-
-
- {isLocked ? (
-
- ) : (
-
- {isProcessing ? (
- <>
-
- Processing...
- >
+
+
+
+
+
+
+ Watermark Text
+ setWatermarkText(e.target.value)}
+ placeholder="e.g., CONFIDENTIAL"
+ className="w-full h-11 px-4 bg-black border border-white/10 text-white rounded-lg focus:ring-2 focus:ring-white/20 outline-none transition-all placeholder:text-zinc-600 uppercase" />
+
+
+ {isLocked ? (
+
) : (
- <>
-
- Generate Preview
- >
+
+ {isProcessing ? <> Processing...> : <> Generate Preview>}
+
)}
-
- )}
-
-
+
+
+ )}
)}
-
- )} {/* end batch.isBatchMode conditional */}
{!batch.isBatchMode && previewUrl && (
From 60621a269b63662d3228ae48ff69fb9cca04db43 Mon Sep 17 00:00:00 2001
From: Ayush Lad
Date: Mon, 20 Apr 2026 18:16:41 +0530
Subject: [PATCH 3/3] chore: remove workflow file from merge
---
.github/workflows/ci.yml | 33 ---------------------------------
1 file changed, 33 deletions(-)
delete mode 100644 .github/workflows/ci.yml
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
deleted file mode 100644
index e2043aa..0000000
--- a/.github/workflows/ci.yml
+++ /dev/null
@@ -1,33 +0,0 @@
-name: CI Pipeline
-
-on:
- push:
- branches:
- - main
- pull_request:
-
-jobs:
- build-and-test:
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
-
- - name: Setup Node.js
- uses: actions/setup-node@v4
- with:
- node-version: '22'
- cache: 'npm'
-
- - name: Install dependencies
- run: npm ci
-
- - name: Run linter
- run: npm run lint
-
- - name: Run tests
- run: npm run test
-
- - name: Build project
- run: npm run build