Skip to content

Added PDF merge preview feature before Download#27

Open
sahasra2212 wants to merge 2 commits into
JhaSourav07:mainfrom
sahasra2212:feat/merge-preview
Open

Added PDF merge preview feature before Download#27
sahasra2212 wants to merge 2 commits into
JhaSourav07:mainfrom
sahasra2212:feat/merge-preview

Conversation

@sahasra2212
Copy link
Copy Markdown
Contributor

#Merge PDF Preview Feature

#Changes Made

  • Added preview-before-download flow for Merge PDF tool
  • Replaced direct auto-download with in-app PDF preview
  • Added "Generate Preview" button
  • Added manual "Download PDF" button after preview
  • Enabled adding more PDFs after initial upload
  • Reset preview when selecting new files
preview

#Tested

  • Uploaded multiple PDFs
  • Reordered files using drag-and-drop
  • Generated preview successfully
  • Verified downloaded file matches preview

Closes #25

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 18, 2026

@sahasra2212 is attempting to deploy a commit to the jhasourav07's projects Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the Merge PDF tool flow to generate an in-app merged PDF preview (via a Blob URL + iframe) before the user downloads the final file, instead of auto-downloading immediately.

Changes:

  • Replace auto-download after merge with a “Generate Preview” step and an embedded iframe preview + manual download.
  • Allow adding more PDFs after initial selection and reset the merged preview when new files are added.
  • Simplify the Merge page UI and state handling around selected items and thumbnail rendering.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/pages/Merge/Merge.jsx
Comment on lines 95 to 103
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 {
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error handling in handleMerge uses a blocking alert() and discards the actual error. The rest of the app surfaces errors inline via component state; consider restoring an error state and rendering it in the page so failures are non-blocking and consistent.

Copilot uses AI. Check for mistakes.
Comment thread src/pages/Merge/Merge.jsx
Comment on lines +154 to +202
{item.thumb ? (
<img src={item.thumb} alt="" />
) : (
<Loader2 className="animate-spin" />
)}
<p className="text-xs truncate">{item.name}</p>
</div>
))}
</div>

{/* Inline add-more zone */}
{/* Drop more */}
<div
onClick={() => 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"
>
<Plus className="w-4 h-4" /> Drop more PDFs or click to add
+ Drop more PDFs or click to add
</div>
</motion.div>
</>
)}

{/* Sticky bottom bar */}
{/* Button */}
{items.length > 0 && (
<div className="fixed bottom-0 inset-x-0 z-40 bg-black/90 backdrop-blur-md border-t border-white/[0.08] px-4 py-3 flex items-center gap-4">
<div className="flex-1 min-w-0">
<p className="text-xs text-zinc-500">{items.length} PDF{items.length !== 1 ? "s" : ""} selected</p>
{items.length < 2
? <p className="text-xs text-zinc-600">Add at least 2 PDFs to merge</p>
: <p className="text-xs text-white font-medium">Order matches the grid — drag cards to rearrange</p>
}
</div>

<button
onClick={clearAll}
className="shrink-0 text-zinc-500 hover:text-white transition-colors text-xs underline underline-offset-4 whitespace-nowrap"
>
Clear all
</button>

{isLocked ? (
<UpgradeButton
reason={lockReason}
limitLabel={lockLabel}
isWalletConnected={isWalletConnected}
isPremium={isPremium}
className="shrink-0 h-11 px-5 text-sm"
/>
<Button onClick={handleMerge} disabled={isProcessing}>
{isProcessing ? (
<>
<Loader2 className="animate-spin mr-2" />
Generating...
</>
) : done ? (
<>
<CheckCircle2 className="mr-2 text-green-500" />
Preview Ready!
</>
) : (
<Button
onClick={handleMerge}
disabled={items.length < 2 || isProcessing}
className="shrink-0 h-11 px-6 text-sm font-bold rounded-xl bg-white text-black hover:bg-zinc-200 transition-all active:scale-[0.98] shadow-xl disabled:opacity-40"
>
{isProcessing
? <><Loader2 className="animate-spin mr-2 w-4 h-4" />Merging…</>
: done
? <><CheckCircle2 className="mr-2 w-4 h-4 text-emerald-500" />Downloaded!</>
: <><Download className="mr-2 w-4 h-4" />Merge & Download</>
}
</Button>
<>
<Download className="mr-2" />
Generate Preview
</>
)}
</div>
</Button>
)}

{/* Preview modal */}
{previewItem && (
<PreviewModal item={previewItem} onClose={() => setPreviewItem(null)} />
{/* Preview */}
{mergedPreviewUrl && (
<div className="mt-6">
<iframe
src={mergedPreviewUrl}
className="w-full h-[400px] bg-white rounded"
/>
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accessibility: thumbnails render with alt="" and the preview iframe has no title. Use a descriptive alt (e.g., the file name) and add a title to the iframe so screen readers can identify the content.

Copilot uses AI. Check for mistakes.
Comment thread src/pages/Merge/Merge.jsx
Comment on lines +204 to +209
<a href={mergedPreviewUrl} download="merged.pdf">
<Button className="mt-3">
<Download className="mr-2" />
Download PDF
</Button>
</a>
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The download UI nests a (Button renders a native button) inside an , which is invalid HTML and creates nested interactive elements. Prefer making the Button trigger a programmatic download, or render a styled element instead of Button for the download link.

Suggested change
<a href={mergedPreviewUrl} download="merged.pdf">
<Button className="mt-3">
<Download className="mr-2" />
Download PDF
</Button>
</a>
<Button
className="mt-3"
onClick={() => {
const link = document.createElement("a");
link.href = mergedPreviewUrl;
link.download = "merged.pdf";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}}
>
<Download className="mr-2" />
Download PDF
</Button>

Copilot uses AI. Check for mistakes.
Comment thread src/pages/Merge/Merge.jsx
Comment on lines 1 to +8
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";
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Several imports are unused now (useFileStore, Layers, X, Trash2, Plus, FileText, motion, AnimatePresence). This increases bundle size and may fail lint/CI. Please remove unused imports or reintroduce the related functionality.

Copilot uses AI. Check for mistakes.
Comment thread src/pages/Merge/Merge.jsx
Comment on lines 175 to +193
{items.length > 0 && (
<div className="fixed bottom-0 inset-x-0 z-40 bg-black/90 backdrop-blur-md border-t border-white/[0.08] px-4 py-3 flex items-center gap-4">
<div className="flex-1 min-w-0">
<p className="text-xs text-zinc-500">{items.length} PDF{items.length !== 1 ? "s" : ""} selected</p>
{items.length < 2
? <p className="text-xs text-zinc-600">Add at least 2 PDFs to merge</p>
: <p className="text-xs text-white font-medium">Order matches the grid — drag cards to rearrange</p>
}
</div>

<button
onClick={clearAll}
className="shrink-0 text-zinc-500 hover:text-white transition-colors text-xs underline underline-offset-4 whitespace-nowrap"
>
Clear all
</button>

{isLocked ? (
<UpgradeButton
reason={lockReason}
limitLabel={lockLabel}
isWalletConnected={isWalletConnected}
isPremium={isPremium}
className="shrink-0 h-11 px-5 text-sm"
/>
<Button onClick={handleMerge} disabled={isProcessing}>
{isProcessing ? (
<>
<Loader2 className="animate-spin mr-2" />
Generating...
</>
) : done ? (
<>
<CheckCircle2 className="mr-2 text-green-500" />
Preview Ready!
</>
) : (
<Button
onClick={handleMerge}
disabled={items.length < 2 || isProcessing}
className="shrink-0 h-11 px-6 text-sm font-bold rounded-xl bg-white text-black hover:bg-zinc-200 transition-all active:scale-[0.98] shadow-xl disabled:opacity-40"
>
{isProcessing
? <><Loader2 className="animate-spin mr-2 w-4 h-4" />Merging…</>
: done
? <><CheckCircle2 className="mr-2 w-4 h-4 text-emerald-500" />Downloaded!</>
: <><Download className="mr-2 w-4 h-4" />Merge & Download</>
}
</Button>
<>
<Download className="mr-2" />
Generate Preview
</>
)}
</div>
</Button>
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The “Generate Preview” button remains enabled when fewer than 2 PDFs are selected; clicking it becomes a no-op due to the early return in handleMerge. Disable the button (and/or show helper text) until at least 2 PDFs are selected to avoid confusing UX.

Copilot uses AI. Check for mistakes.
Comment thread src/pages/Merge/Merge.jsx
Comment on lines +63 to 72
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
}));
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addFiles filters strictly on file.type === "application/pdf". Drag-and-dropped PDFs sometimes have an empty/unknown MIME type, so valid PDFs can be ignored with no feedback. Consider accepting by extension as a fallback and/or showing an inline message when files are skipped.

Copilot uses AI. Check for mistakes.
Comment thread src/pages/Merge/Merge.jsx
Comment on lines +150 to 162
{/* Grid */}
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4 mb-6">
{items.map(item => (
<div key={item.id} className="bg-zinc-800 p-2 rounded">
{item.thumb ? (
<img src={item.thumb} alt="" />
) : (
<Loader2 className="animate-spin" />
)}
<p className="text-xs truncate">{item.name}</p>
</div>
))}
</div>
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The item grid removed reordering/removal controls and per-file preview, which conflicts with the PR description (“Reordered files using drag-and-drop”) and the previous Merge UX implied by the deleted code. If those features are still desired, they should be preserved while adding the merged-output preview step.

Copilot uses AI. Check for mistakes.
Comment thread src/pages/Merge/Merge.jsx
Comment on lines +156 to +157
) : (
<Loader2 className="animate-spin" />
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If renderFirstPage fails, thumb stays null but loading is set to false; the grid currently renders a spinner whenever thumb is falsy, so failures will show an infinite loading indicator. Use item.loading to decide when to show the spinner, and render a placeholder state when thumbnail generation fails.

Suggested change
) : (
<Loader2 className="animate-spin" />
) : item.loading ? (
<div className="flex items-center justify-center h-24">
<Loader2 className="animate-spin" />
</div>
) : (
<div className="flex flex-col items-center justify-center h-24 text-zinc-400">
<FileText className="mb-1" />
<span className="text-xs">Preview unavailable</span>
</div>

Copilot uses AI. Check for mistakes.
Comment thread src/pages/Merge/Merge.jsx
// ─── Main Component ────────────────────────────────────────────────────────────
export function Merge() {
const [items, setItems] = useFileStore("Merge_items", []);
const [items, setItems] = useState([]);
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merge now uses local component state for selected PDFs, but the rest of the tools persist selected files via useFileStore (IndexedDB) so refresh/navigation doesn’t wipe progress. Consider switching back to useFileStore for consistency and to preserve the previous behavior for Merge as well.

Suggested change
const [items, setItems] = useState([]);
const [items, setItems] = useFileStore("merge-items", []);

Copilot uses AI. Check for mistakes.
Comment thread src/pages/Merge/Merge.jsx
Comment on lines +88 to 101
// 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 {
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The subscription/free-tier gating and usage tracking for Merge appears to have been removed (no useSubscription/FREE_LIMITS checks and no incrementUsage call after a successful merge). This likely breaks global request limits and file-count limits for non-premium users; other tools consistently enforce these.

Copilot uses AI. Check for mistakes.
@JhaSourav07
Copy link
Copy Markdown
Owner

Kindly address copilot comments
@sahasra2212

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(merge): add reusable PDF preview modal before download

3 participants