diff --git a/src/pages/Merge/Merge.jsx b/src/pages/Merge/Merge.jsx
index 4326fbf..25f6e7e 100644
--- a/src/pages/Merge/Merge.jsx
+++ b/src/pages/Merge/Merge.jsx
@@ -19,7 +19,7 @@ pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
import.meta.url,
).toString();
-// ─── helpers ──────────────────────────────────────────────────────────────────
+// ─── helpers
let _uid = 0;
function makeId() { return ++_uid; }
@@ -58,10 +58,10 @@ function PdfCard({ item, onRemove, onPreview, onDragStart, onDragEnter, onDragEn
onDragOver={(e) => e.preventDefault()}
onDrop={() => { onDragEnd(); setOver(false); }}
className={[
- "relative group flex flex-col items-center gap-2 border rounded-2xl p-2.5 cursor-grab active:cursor-grabbing select-none transition-all duration-150",
+ "relative group flex flex-col items-center gap-3 border rounded-2xl p-3 cursor-grab active:cursor-grabbing select-none transition-all duration-150",
over
? "border-white/40 bg-white/[0.06] scale-[1.02]"
- : "bg-zinc-900/60 border-white/[0.06] hover:border-white/20",
+ : "bg-zinc-900/60 border-white/[0.06] hover:border-white/30 hover:shadow-[0_0_20px_rgba(255,255,255,0.05)] hover:scale-[1.02]",
].join(" ")}
>
{/* Order badge */}
@@ -73,7 +73,7 @@ function PdfCard({ item, onRemove, onPreview, onDragStart, onDragEnter, onDragEn
{/* Thumbnail area */}
{item.thumb ? (
@@ -111,10 +111,20 @@ function PdfCard({ item, onRemove, onPreview, onDragStart, onDragEnter, onDragEn
- {/* Name + size */}
-
-
{item.name}
-
{formatFileSize(item.size)}{item.numPages != null && ` · ${item.numPages} pages`}
+ {/* File Info */}
+
+
+ {/* File name */}
+
+ {item.name}
+
+
+ {/* Metadata */}
+
+ {formatFileSize(item.size)}
+ {item.numPages != null && ` • ${item.numPages} pages`}
+
+
{/* Remove */}
diff --git a/src/pages/Watermark/Watermark.jsx b/src/pages/Watermark/Watermark.jsx
index 7f82491..edc2a85 100644
--- a/src/pages/Watermark/Watermark.jsx
+++ b/src/pages/Watermark/Watermark.jsx
@@ -1,6 +1,6 @@
-import React, { useState, useEffect, useRef } from "react";
+import React, { useState, useEffect } from "react";
import { useFileStore } from "../../hooks/useFileStore";
-import { Stamp, X, Download, Loader2, Settings2 } from "lucide-react";
+import { Stamp, X, Download, Loader2 } from "lucide-react";
import { Button } from "../../components/ui/Button";
import { UpgradeButton } from "../../components/ui/UpgradeButton";
import { addWatermark, getPdfPageCount } from "../../services/pdf.service";
@@ -9,23 +9,16 @@ import { formatFileSize } from "../../utils/formatters";
import { useSubscription } from "../../hooks/useSubscription";
import { FREE_LIMITS, mbToBytes } from "../../config/limits";
+
+
export function Watermark() {
const [file, setFile] = useFileStore("Watermark_file", null);
const [watermarkText, setWatermarkText] = useState("CONFIDENTIAL");
const [isProcessing, setIsProcessing] = useState(false);
+ const [error, setError] = useState(null);
const [originalPreviewUrl, setOriginalPreviewUrl] = useState(null);
const [previewUrl, setPreviewUrl] = useState(null);
const [pageCount, setPageCount] = useState(0);
- const lastUrlRef = useRef(null);
-
- const [options, setOptions] = useState({
- position: "center",
- opacity: 0.3,
- fontSize: 60,
- rotation: 45,
- offsetX: 0,
- offsetY: 0
- });
const {
isPremium,
@@ -34,240 +27,244 @@ export function Watermark() {
isWalletConnected,
} = useSubscription();
- // 1. Fetch Page Count ONLY when a file exists
useEffect(() => {
- if (!file) return; // Just exit, don't set state here
-
- const fetchPageCount = async () => {
- try {
- const count = await getPdfPageCount(file);
- setPageCount(count);
- } catch (err) {
- console.error("Failed to get page count:", err);
+ return () => {
+ if (originalPreviewUrl) {
+ URL.revokeObjectURL(originalPreviewUrl);
+ }
+ if (previewUrl) {
+ URL.revokeObjectURL(previewUrl);
}
};
+ }, [originalPreviewUrl, previewUrl]);
- fetchPageCount();
- }, [file]);
-
+ // Recreate original preview URL when file is loaded from storage but preview is missing
useEffect(() => {
- if (!file || !watermarkText.trim()) {
- const t = setTimeout(() => setPreviewUrl(null), 0);
- return () => clearTimeout(t);
- }
-
- const timer = setTimeout(async () => {
- try {
- setIsProcessing(true);
- const blob = await addWatermark(file, watermarkText, options);
- const url = URL.createObjectURL(blob);
-
- if (lastUrlRef.current) URL.revokeObjectURL(lastUrlRef.current);
- lastUrlRef.current = url;
- setPreviewUrl(url);
- } catch (err) {
- console.error("Preview failed", err);
- } finally {
- setIsProcessing(false);
- }
- }, 500);
+ if (!file || previewUrl || originalPreviewUrl) return;
- return () => clearTimeout(timer);
- }, [file, watermarkText, options]);
+ const originalUrl = file.url || URL.createObjectURL(file);
+ queueMicrotask(() => setOriginalPreviewUrl(originalUrl));
- // 3. Handle cleanup on unmount
- useEffect(() => {
return () => {
- if (originalPreviewUrl) URL.revokeObjectURL(originalPreviewUrl);
- if (lastUrlRef.current) URL.revokeObjectURL(lastUrlRef.current);
+ if (!file.url) {
+ URL.revokeObjectURL(originalUrl);
+ }
};
- }, [originalPreviewUrl]);
+ }, [file, previewUrl, originalPreviewUrl]);
+
+ const fileTooLarge =
+ !isPremium &&
+ file &&
+ file.size > mbToBytes(FREE_LIMITS.watermark.maxFileSizeMb);
- // 4. Initialize original preview
- useEffect(() => {
- if (!file || originalPreviewUrl) return;
- const url = file.url || URL.createObjectURL(file);
- const t = setTimeout(() => setOriginalPreviewUrl(url), 0);
- return () => clearTimeout(t);
- }, [file, originalPreviewUrl]);
-
- // Subscription & Lock Logic
- 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 handleFileSelected = (selectedFiles) => {
- const selectedFile = selectedFiles[0];
- if (!selectedFile || selectedFile.type !== "application/pdf") return;
- setFile(selectedFile);
- setPreviewUrl(null);
- if (originalPreviewUrl) URL.revokeObjectURL(originalPreviewUrl);
- setOriginalPreviewUrl(URL.createObjectURL(selectedFile));
- };
+ const lockLabel = fileTooLarge
+ ? `${FREE_LIMITS.watermark.maxFileSizeMb} MB`
+ : undefined;
- const clearFile = () => {
- setFile(null);
- setPreviewUrl(null);
- setOriginalPreviewUrl(null);
- setPageCount(0);
- };
+ const handleFileSelected = async (selectedFiles) => {
+ const selectedFile = selectedFiles[0];
+ if (!selectedFile) return;
- const updateOption = (key, value) => {
- setOptions(prev => ({ ...prev, [key]: value }));
- };
+ if (selectedFile.type !== "application/pdf") {
+ setError("Please upload a valid PDF file.");
+ return;
+ }
- const handleDownload = async () => {
- if (!previewUrl) return;
- await incrementUsage();
- const link = document.createElement("a");
- link.href = previewUrl;
- link.download = `QuickPDF_Watermarked_${Date.now()}.pdf`;
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
+ setError(null);
+ setFile(selectedFile);
+
+ const count = await getPdfPageCount(selectedFile);
+
+ setPageCount(count);
+ 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);
+ await incrementUsage();
+ } catch {
+ setError("Could not read the PDF file. It might be corrupted or encrypted.");
+ setFile(null);
+ } finally {
+ setIsProcessing(false);
+ }
};
+ function clearFile() {
+ setFile(null);
+ setError(null);
+ setPreviewUrl(null);
+ setPageCount(0);
+}
return (
-
+
-
Add Watermark
-
Secure browser-based watermarking.
+
+
+ Add Watermark
+
+
+
+ Stamp text across your document securely in your browser.
+ {!isPremium && (
+
+ Free tier: files up to{" "}
+ {FREE_LIMITS.watermark.maxFileSizeMb} MB
+
+ )}
+
-
+
- {!file ? (
-
- ) : (
-
-
-
-
{file.name}
-
- {formatFileSize(file.size)}
- •
- {pageCount} {pageCount === 1 ? 'page' : 'pages'}
-
-
-
-
-
-
+ {error && (
+
+ {error}
+
+ )}
-
- Watermark Text
- setWatermarkText(e.target.value)}
- className="w-full h-11 px-4 bg-black border border-white/10 text-white rounded-lg uppercase outline-none focus:ring-1 focus:ring-white/20"
- />
-
+ {!file ? (
+
+ ) : (
+
+
+
+
+ {file.name}
+
-
-
-
-
Style & Position
-
-
-
- Position
- updateOption("position", e.target.value)}
- className="w-full h-10 px-3 bg-black border border-white/10 text-white rounded-lg outline-none"
- >
- Center
- Top Left
- Top Right
- Bottom Left
- Bottom Right
-
-
-
-
-
-
- Rotation: {options.rotation}°
- updateOption("rotation", parseInt(e.target.value))} className="w-full h-1.5 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-white" />
-
-
-
+
+ {formatFileSize(file.size)} • {pageCount} pages
+ {fileTooLarge && (
+
+ (exceeds free limit)
+
+ )}
+
+
+
+
+
- )}
-
- {file && (
-
-
- {isProcessing ? "Updating Preview..." : previewUrl ? "Preview with Watermark" : "Original PDF"}
-
-
-
- )}
+
+
+ Watermark Text
+
- {previewUrl && (
-
-
+ 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...>
+ <>
+
+ Processing...
+ >
) : (
- <> Download Watermarked PDF>
+ <>
+
+ Apply Watermark
+ >
)}
)}
+
+
+ )}
+
+
+ {originalPreviewUrl && !previewUrl && (
+
+
+ Original PDF
+
+
+
+
+ )}
+
+ {previewUrl && (
+
+
+ Preview with Watermark
+
+
+
+
+
+ {
+ const link = document.createElement("a");
+ link.href = previewUrl;
+ link.download = `QuickPDF_Watermarked_${Date.now()}.pdf`;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ }}
+ >
+
+ Download PDF
+
+
)}
);
-}
\ No newline at end of file
+}