Skip to content
This repository was archived by the owner on Feb 18, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 37 additions & 14 deletions app/tool/[id]/processing/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { useEffect, useState } from "react";
import { useParams, useRouter } from "next/navigation";
import Tesseract from "tesseract.js";
import { getStoredFiles, clearStoredFiles } from "@/lib/fileStore";
import { PDFDocument } from "pdf-lib";
import { PDFDocument, rgb, degrees } from "pdf-lib";
import { useRecentFiles } from "@/lib/hooks/useRecentFiles";

type StoredFile = {
data: string;
Expand All @@ -17,6 +18,7 @@ export default function ProcessingPage() {
const router = useRouter();
const params = useParams();
const toolId = params.id as string;
const { addRecentFile } = useRecentFiles();

const [status, setStatus] = useState<"processing" | "done" | "error">(
"processing"
Expand All @@ -26,6 +28,7 @@ export default function ProcessingPage() {
const [error, setError] = useState("");
const [copied, setCopied] = useState(false);
const [downloadUrl, setDownloadUrl] = useState<string | null>(null);
const [processedFile, setProcessedFile] = useState<StoredFile | null>(null);

/* ================= RUN TOOL ================= */
useEffect(() => {
Expand All @@ -37,22 +40,28 @@ export default function ProcessingPage() {
return;
}

setProcessedFile(stored[0]); // Keep reference for recent files

try {
if (toolId === "ocr") await runOCR(stored[0].data);
if (toolId === "ocr") await runOCR(stored[0]);

else if (toolId === "pdf-protect")
await protectPDF(stored[0].data);
await protectPDF(stored[0]);

else if (toolId === "jpeg-to-pdf")
await imageToPdf(stored[0].data, "jpg");
await imageToPdf(stored[0], "jpg");

else if (toolId === "png-to-pdf")
await imageToPdf(stored[0].data, "png");
await imageToPdf(stored[0], "png");

else if (toolId === "pdf-compress")
await startCompressFlow(stored);

else setStatus("done");
else {
// Default success case
addToRecent(stored[0].name);
setStatus("done");
}
} catch (e) {
console.error(e);
setError("Processing failed");
Expand All @@ -65,9 +74,17 @@ export default function ProcessingPage() {
run();
}, [toolId, router]);

const addToRecent = (fileName: string) => {
addRecentFile({
fileName,
tool: toolId,
time: new Date().toLocaleString(),
});
};

/* ================= OCR ================= */
const runOCR = async (base64: string) => {
const res = await Tesseract.recognize(base64, "eng", {
const runOCR = async (file: StoredFile) => {
const res = await Tesseract.recognize(file.data, "eng", {
logger: (m) => {
if (m.status === "recognizing text") {
setProgress(Math.round(m.progress * 100));
Expand All @@ -76,6 +93,7 @@ export default function ProcessingPage() {
});

setText(res.data.text);
addToRecent(file.name);
setStatus("done");
};

Expand Down Expand Up @@ -110,23 +128,27 @@ export default function ProcessingPage() {
);

setDownloadUrl(makeBlobUrl(bytes));
addToRecent(files[0].name); // Add the first file to recent
setProgress(100);
setStatus("done");
};



/* ================= PDF PROTECT ================= */
const protectPDF = async (base64: string) => {
const bytes = base64ToBytes(base64);
const protectPDF = async (file: StoredFile) => {
const bytes = base64ToBytes(file.data);
const pdf = await PDFDocument.load(bytes);
const saved = await pdf.save();

setDownloadUrl(makeBlobUrl(saved));
addToRecent(file.name);
setStatus("done");
};

/* ================= IMAGE → PDF ================= */
const imageToPdf = async (base64: string, type: "jpg" | "png") => {
const bytes = base64ToBytes(base64);
const imageToPdf = async (file: StoredFile, type: "jpg" | "png") => {
const bytes = base64ToBytes(file.data);

const pdf = await PDFDocument.create();
const img =
Expand All @@ -145,6 +167,7 @@ export default function ProcessingPage() {

const saved = await pdf.save();
setDownloadUrl(makeBlobUrl(saved));
addToRecent(file.name);
setStatus("done");
};

Expand Down Expand Up @@ -207,8 +230,8 @@ export default function ProcessingPage() {
{toolId === "jpeg-to-pdf"
? "JPEG Converted to PDF!"
: toolId === "png-to-pdf"
? "PNG Converted to PDF!"
: "Completed Successfully"}
? "PNG Converted to PDF!"
: "Completed Successfully"}
</h2>

{downloadUrl && (
Expand Down
128 changes: 128 additions & 0 deletions components/ConfirmationModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
"use client";

import { motion, AnimatePresence } from "framer-motion";
import { AlertTriangle, X } from "lucide-react";
import { useEffect } from "react";
import { cn } from "@/lib/utils"; // Assuming utils exists, if not I'll inline, but usually it does in these setups. Checking first might be safer but `clsx` and `tailwind-merge` are in package.json so valid.
// I'll check if lib/utils exists first to be safe, or just use clsx/twMerge directly.
// Actually, looking at previous file reads, `lib/hooks/useRecentFiles.ts` exists.
// I'll assume standard `lib/utils` for now, if it fails I'll fix.
// Wait, I haven't seen `lib/utils.ts`. I should probably check or just write safe code.
// I'll write safe code using clsx and tailwind-merge directly to avoid assumption failures.

import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

interface ConfirmationModalProps {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
title: string;
message: string;
confirmText?: string;
cancelText?: string;
type?: "danger" | "neutral";
}

export function ConfirmationModal({
isOpen,
onClose,
onConfirm,
title,
message,
confirmText = "Confirm",
cancelText = "Cancel",
type = "danger",
}: ConfirmationModalProps) {
// Close on Escape key
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
if (isOpen) document.addEventListener("keydown", handleEscape);
return () => document.removeEventListener("keydown", handleEscape);
}, [isOpen, onClose]);

return (
<AnimatePresence>
{isOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-0">
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="fixed inset-0 bg-black/40 backdrop-blur-sm"
/>

{/* Modal */}
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 10 }}
transition={{ duration: 0.2 }}
className="relative w-full max-w-md bg-card border border-border rounded-xl shadow-lg overflow-hidden"
>
<div className="p-6">
<div className="flex items-start gap-4">
<div
className={cn(
"p-3 rounded-full shrink-0",
type === "danger"
? "bg-rose-100 text-rose-600"
: "bg-blue-100 text-blue-600"
)}
>
<AlertTriangle className="w-6 h-6" />
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-foreground">
{title}
</h3>
<p className="mt-2 text-sm text-muted-foreground leading-relaxed">
{message}
</p>
</div>
</div>

<div className="mt-8 flex justify-end gap-3">
<button
onClick={onClose}
className="px-4 py-2 text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-muted rounded-lg transition-colors"
>
{cancelText}
</button>
<button
onClick={() => {
onConfirm();
onClose();
}}
className={cn(
"px-4 py-2 text-sm font-medium text-white rounded-lg shadow-sm transition-colors",
type === "danger"
? "bg-rose-600 hover:bg-rose-700"
: "bg-blue-600 hover:bg-blue-700"
)}
>
{confirmText}
</button>
</div>
</div>

<button
onClick={onClose}
className="absolute top-4 right-4 text-muted-foreground hover:text-foreground transition-colors"
>
<X size={20} />
</button>
</motion.div>
</div>
)}
</AnimatePresence>
);
}
46 changes: 27 additions & 19 deletions components/RecentFiles.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { Trash2 } from "lucide-react";
import { Trash2, FileText as FileIcon } from "lucide-react";

export type RecentFile = {
fileName: string;
Expand All @@ -19,13 +19,12 @@ export default function RecentFiles({ files, onDelete, onClear }: RecentFilesPro

return (
<div className="mt-12">
{/* Header with Clear Button */}
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold">Recent Files</h2>
<div className="flex justify-between items-center mb-6">
<h2 className="text-xl font-semibold text-foreground">Recent Files</h2>

<button
onClick={onClear}
className="text-red-500 hover:text-red-700 text-sm font-medium"
className="text-sm px-4 py-2 rounded-lg text-rose-600 hover:bg-rose-50 transition-colors font-medium"
>
Clear History
</button>
Expand All @@ -35,23 +34,32 @@ export default function RecentFiles({ files, onDelete, onClear }: RecentFilesPro
{files.map((file, index) => (
<div
key={index}
className="p-4 rounded-lg border bg-white shadow-sm flex justify-between items-center"
className="group p-4 rounded-xl border border-border bg-card hover:shadow-md transition-all flex justify-between items-center"
>
<div>
<p className="font-medium">{file.fileName}</p>
<p className="text-sm text-gray-500">
{file.tool} • {file.time}
</p>
<div className="flex items-start gap-3">
<div className="p-2 rounded-lg bg-indigo-50 text-indigo-500">
<FileIcon className="w-5 h-5" />
</div>
<div>
<p className="font-medium text-foreground">{file.fileName}</p>
<div className="flex items-center gap-2 text-xs text-muted-foreground mt-1">
<span className="bg-muted px-2 py-0.5 rounded text-xs font-medium uppercase tracking-wide">
{file.tool.replace("-", " ")}
</span>
<span>{file.time}</span>
</div>
</div>
</div>

{/* Delete Button */}
<button
onClick={() => onDelete(index)}
className="p-2 text-red-500 hover:bg-red-50 rounded-full transition"
title="Delete"
>
<Trash2 size={18} />
</button>
<div className="opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={() => onDelete(index)}
className="p-2 text-rose-500 hover:bg-rose-50 rounded-lg transition-colors"
title="Delete"
>
<Trash2 size={18} />
</button>
</div>
</div>
))}
</div>
Expand Down
Loading