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} +
+ )} -
- - 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

-
- -
- - -
- -
-
- - updateOption("fontSize", parseInt(e.target.value))} className="w-full h-1.5 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-white" /> -
-
- - updateOption("opacity", parseFloat(e.target.value))} className="w-full h-1.5 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-white" /> -
-
- -
- - updateOption("rotation", parseInt(e.target.value))} className="w-full h-1.5 bg-zinc-800 rounded-lg appearance-none cursor-pointer accent-white" /> -
- -
-
- - updateOption("offsetX", parseInt(e.target.value) || 0)} - className="w-full h-10 px-3 bg-black border border-white/10 text-white rounded-lg outline-none focus:ring-1 focus:ring-white/20" - /> -
-
- - updateOption("offsetY", parseInt(e.target.value) || 0)} - className="w-full h-10 px-3 bg-black border border-white/10 text-white rounded-lg outline-none focus:ring-1 focus:ring-white/20" - /> -
-
+ + {formatFileSize(file.size)} • {pageCount} pages + {fileTooLarge && ( + + (exceeds free limit) + + )} +
+ +
- )} -
- {file && ( -
-

- {isProcessing ? "Updating Preview..." : previewUrl ? "Preview with Watermark" : "Original PDF"} -

-