diff --git a/src/pages/Merge/Merge.jsx b/src/pages/Merge/Merge.jsx
index b9df87d..09e5420 100644
--- a/src/pages/Merge/Merge.jsx
+++ b/src/pages/Merge/Merge.jsx
@@ -1,17 +1,13 @@
import React, { useState, useRef, useCallback, useEffect } from "react";
import { useFileStore } from "../../hooks/useFileStore";
import {
- Layers, X, Download, Loader2, Trash2, GripVertical,
- Plus, Eye, EyeOff, CheckCircle2, FileText,
+ Layers, X, Download, Loader2, Trash2,
+ Plus, CheckCircle2, FileText,
} from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
-import { Button } from "../../components/ui/Button";
-import { UpgradeButton } from "../../components/ui/UpgradeButton";
-import { mergePdfs } from "../../services/pdf.service";
-import { Dropzone } from "../../components/pdf/Dropzone";
-import { formatFileSize } from "../../utils/formatters";
-import { useSubscription } from "../../hooks/useSubscription";
-import { FREE_LIMITS } from "../../config/limits";
+import { Button } from "../../components/ui/Button";
+import { mergePdfs } from "../../services/pdf.service";
+import { Dropzone } from "../../components/pdf/Dropzone";
import * as pdfjsLib from "pdfjs-dist";
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
@@ -19,7 +15,7 @@ pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
import.meta.url,
).toString();
-// ─── helpers ──────────────────────────────────────────────────────────────────
+// ─── helpers ─────────────────────────
let _uid = 0;
function makeId() { return ++_uid; }
@@ -29,443 +25,189 @@ async function renderFirstPage(file) {
const pdf = await pdfjsLib.getDocument({ data: buf }).promise;
const page = await pdf.getPage(1);
const viewport = page.getViewport({ scale: 0.5 });
+
const canvas = document.createElement("canvas");
- canvas.width = viewport.width;
+ canvas.width = viewport.width;
canvas.height = viewport.height;
- await page.render({ canvasContext: canvas.getContext("2d"), viewport }).promise;
+
+ await page.render({
+ canvasContext: canvas.getContext("2d"),
+ viewport
+ }).promise;
+
const url = canvas.toDataURL("image/jpeg", 0.8);
- canvas.width = 0;
- return { thumb: url, numPages: pdf.numPages };
+ return { thumb: url };
} catch {
- return { thumb: null, numPages: null };
+ return { thumb: null };
}
}
-// ─── PDF Card ─────────────────────────────────────────────────────────────────
-function PdfCard({ item, onRemove, onPreview, onDragStart, onDragEnter, onDragEnd }) {
- const [over, setOver] = useState(false);
-
- return (
- onDragStart(item.id)}
- onDragEnter={() => { onDragEnter(item.id); setOver(true); }}
- onDragLeave={() => setOver(false)}
- 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",
- over
- ? "border-white/40 bg-white/[0.06] scale-[1.02]"
- : "bg-zinc-900/60 border-white/[0.06] hover:border-white/20",
- ].join(" ")}
- >
- {/* Order badge */}
-
- {item.order}
-
-
-
-
- {/* Thumbnail area */}
-
- {item.thumb ? (
-
- ) : item.loadingThumb ? (
-
-
- Loading…
-
- ) : (
-
-
-
- )}
-
- {/* Page count overlay */}
- {item.numPages != null && (
-
- {item.numPages}p
-
- )}
-
- {/* Preview overlay button */}
-
{ e.stopPropagation(); onPreview(item); }}
- className="absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity rounded-xl"
- title="Preview PDF"
- >
-
-
-
-
- {/* Name + size */}
-
-
{item.name}
-
{formatFileSize(item.size)}
-
-
- {/* Remove */}
- { e.stopPropagation(); onRemove(item.id); }}
- className="absolute top-1.5 right-1.5 w-6 h-6 flex items-center justify-center rounded-full bg-black/80 text-zinc-500 hover:text-red-400 opacity-0 group-hover:opacity-100 transition-all"
- aria-label="Remove"
- >
-
-
-
- );
-}
-
-// ─── Preview Modal ─────────────────────────────────────────────────────────────
-function PreviewModal({ item, onClose }) {
- const objectUrl = useRef(null);
- const [src, setSrc] = useState(null);
-
- useEffect(() => {
- if (!item) return;
- const url = URL.createObjectURL(item.file);
- objectUrl.current = url;
- setSrc(url);
- return () => { URL.revokeObjectURL(url); };
- }, [item]);
-
- if (!item) return null;
-
- return (
-
-
- e.stopPropagation()}
- >
- {/* Header */}
-
-
-
- {item.name}
- {item.numPages && (
- · {item.numPages} pages
- )}
-
-
-
-
-
-
- {/* Iframe viewer */}
-
- {src ? (
-
- ) : (
-
-
-
- )}
-
-
-
-
- );
-}
-
-// ─── Main Component ────────────────────────────────────────────────────────────
export function Merge() {
- const [items, setItems] = useFileStore("Merge_items", []);
+ const [items, setItems] = useState([]);
const [isProcessing, setIsProcessing] = useState(false);
- const [done, setDone] = useState(false);
- const [error, setError] = useState(null);
- const [previewItem, setPreviewItem] = useState(null);
+ const [done, setDone] = useState(false);
+ const [mergedPreviewUrl, setMergedPreviewUrl] = useState(null);
const fileInputRef = useRef(null);
- const dragId = useRef(null);
- const overId = useRef(null);
-
- const { isPremium, hasReachedGlobalLimit, incrementUsage, isWalletConnected } = useSubscription();
-
- const fileLimitExceeded = !isPremium && items.length > FREE_LIMITS.merge.maxFiles;
- const isLocked = hasReachedGlobalLimit || fileLimitExceeded;
- const lockReason = hasReachedGlobalLimit ? "global" : "size";
- const lockLabel = fileLimitExceeded ? `${FREE_LIMITS.merge.maxFiles} files max` : undefined;
-
- // ── add files ──
- const addFiles = useCallback(async (selectedFiles) => {
- const validPdfs = selectedFiles.filter((f) => f.type === "application/pdf");
- if (validPdfs.length !== selectedFiles.length) {
- setError("Some files were ignored — only PDF files are allowed.");
- } else {
- setError(null);
- }
- setDone(false);
- // Build skeleton items first so the UI updates immediately
- const newItems = validPdfs.map((file) => ({
+ // cleanup URL
+ useEffect(() => {
+ return () => {
+ if (mergedPreviewUrl) {
+ URL.revokeObjectURL(mergedPreviewUrl);
+ }
+ };
+ }, [mergedPreviewUrl]);
+
+ // add files
+ const addFiles = useCallback(async (files) => {
+ const valid = files.filter(f => f.type === "application/pdf");
+
+ const newItems = valid.map(file => ({
id: makeId(),
file,
name: file.name,
- size: file.size,
thumb: null,
- numPages: null,
- loadingThumb: true,
+ loading: true
}));
- setItems((prev) => [...prev, ...newItems]);
+ setItems(prev => [...prev, ...newItems]);
+ setMergedPreviewUrl(null);
+ setDone(false);
- // Render thumbnails asynchronously one by one
for (const item of newItems) {
- const { thumb, numPages } = await renderFirstPage(item.file);
- setItems((prev) =>
- prev.map((it) =>
- it.id === item.id ? { ...it, thumb, numPages, loadingThumb: false } : it
+ const { thumb } = await renderFirstPage(item.file);
+ setItems(prev =>
+ prev.map(it =>
+ it.id === item.id ? { ...it, thumb, loading: false } : it
)
);
}
}, []);
- // ── remove / clear ──
- function removeItem(id) {
- setItems((prev) => prev.filter((it) => it.id !== id));
- setDone(false);
- }
+ // merge
+ const handleMerge = async () => {
+ if (items.length < 2) return;
- function clearAll() {
- setItems([]);
- setError(null);
+ setIsProcessing(true);
setDone(false);
- }
- // ── drag-and-drop reorder ──
- function handleDragStart(id) { dragId.current = id; }
- function handleDragEnter(id) { overId.current = id; }
- function handleDragEnd() {
- if (!dragId.current || !overId.current || dragId.current === overId.current) return;
- setItems((prev) => {
- const arr = [...prev];
- const from = arr.findIndex((it) => it.id === dragId.current);
- const to = arr.findIndex((it) => it.id === overId.current);
- const [moved] = arr.splice(from, 1);
- arr.splice(to, 0, moved);
- return arr;
- });
- dragId.current = null;
- overId.current = null;
- }
-
- // ── merge ──
- const handleMerge = async () => {
- if (items.length < 2) return;
try {
- setIsProcessing(true);
- setError(null);
- setDone(false);
- const mergedBlob = await mergePdfs(items.map((it) => it.file));
- const url = URL.createObjectURL(mergedBlob);
- const link = document.createElement("a");
- link.href = url;
- link.download = `QuickPDF_Merged_${Date.now()}.pdf`;
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
- URL.revokeObjectURL(url);
- await incrementUsage();
+ const blob = await mergePdfs(items.map(i => i.file));
+ const url = URL.createObjectURL(blob);
+
+ setMergedPreviewUrl(url);
setDone(true);
} catch {
- setError("An error occurred while merging the PDFs. Please try again.");
+ alert("Error merging PDFs");
} finally {
setIsProcessing(false);
}
};
- const orderedItems = items.map((item, i) => ({ ...item, order: i + 1 }));
-
return (
-
-
- {/* Header */}
-
-
-
-
-
Merge PDF
-
- Combine multiple PDFs into a single file. Drag thumbnails to set the order.
- {!isPremium && (
-
- Free tier: up to {FREE_LIMITS.merge.maxFiles} files · {FREE_LIMITS.globalRequests} total actions
-
- )}
-
-
-
- {/* Error */}
-
- {error && (
-
-
- {error}
-
-
- )}
-
+
+
+ {/* Hidden Input */}
+
{
+ if (e.target.files) addFiles(Array.from(e.target.files));
+ e.target.value = "";
+ }}
+ />
+
+
Merge PDF
{items.length === 0 ? (
-
+
) : (
-
-
+ <>
{/* Toolbar */}
-
-
- {items.length} file{items.length !== 1 ? "s" : ""}
- {!isPremium && / {FREE_LIMITS.merge.maxFiles} max }
- · drag cards to reorder · hover for preview
-
-
-
fileInputRef.current?.click()}
- className="flex items-center gap-1.5 h-8 px-3 rounded-xl bg-white/5 border border-white/10 hover:bg-white/10 transition-all text-zinc-300 hover:text-white text-xs font-medium"
- >
- Add more
-
-
- Clear all
-
-
-
{
- if (e.target.files) addFiles(Array.from(e.target.files));
- e.target.value = "";
+
+ fileInputRef.current?.click()}
+ className="px-3 py-1 bg-white/10 rounded text-sm"
+ >
+ + Add more
+
+
+ {
+ setItems([]);
+ setMergedPreviewUrl(null);
}}
- />
+ className="px-3 py-1 bg-red-500/20 rounded text-red-400 text-sm"
+ >
+ Clear all
+
- {/* Thumbnail grid */}
-
-
- {orderedItems.map((item) => (
-
- ))}
-
+ {/* Grid */}
+
+ {items.map(item => (
+
+ {item.thumb ? (
+
+ ) : (
+
+ )}
+
{item.name}
+
+ ))}
- {/* Inline add-more zone */}
+ {/* Drop more */}
fileInputRef.current?.click()}
- className="flex items-center justify-center h-24 border-2 border-dashed border-white/10 rounded-2xl text-zinc-600 hover:border-white/25 hover:text-zinc-400 transition-all cursor-pointer text-sm gap-2"
+ className="border-2 border-dashed border-white/20 p-6 text-center text-zinc-400 cursor-pointer mb-6"
>
-
Drop more PDFs or click to add
+ + Drop more PDFs or click to add
-
+ >
)}
- {/* Sticky bottom bar */}
+ {/* Button */}
{items.length > 0 && (
-
-
-
{items.length} PDF{items.length !== 1 ? "s" : ""} selected
- {items.length < 2
- ?
Add at least 2 PDFs to merge
- :
Order matches the grid — drag cards to rearrange
- }
-
-
-
- Clear all
-
-
- {isLocked ? (
-
+
+ {isProcessing ? (
+ <>
+
+ Generating...
+ >
+ ) : done ? (
+ <>
+
+ Preview Ready!
+ >
) : (
-
- {isProcessing
- ? <> Merging…>
- : done
- ? <> Downloaded!>
- : <> Merge & Download>
- }
-
+ <>
+
+ Generate Preview
+ >
)}
-
+
)}
- {/* Preview modal */}
- {previewItem && (
-
setPreviewItem(null)} />
+ {/* Preview */}
+ {mergedPreviewUrl && (
+
)}
);