feat: Revamp UI with new background and component styling, and add copy-to-clipboard and reset options.

This commit is contained in:
2025-12-10 01:59:05 -05:00
parent f4aa45e527
commit 95d6bfa084
7 changed files with 175 additions and 53 deletions

View File

@@ -3,34 +3,33 @@ import { Navbar } from "~/components/navbar";
export default function HomePage() {
return (
<main className="relative min-h-screen w-full bg-background selection:bg-primary/10">
<main className="relative min-h-screen w-full selection:bg-primary/10">
{/* Background Pattern */}
{/* Background Pattern */}
<div className="fixed inset-0 -z-10 h-full w-full bg-white dark:bg-neutral-950 overflow-hidden">
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:24px_24px]"></div>
{/* Animated Blobs */}
<div className="absolute top-0 left-1/4 w-72 h-72 bg-purple-300 dark:bg-purple-600 rounded-full mix-blend-multiply dark:mix-blend-screen filter blur-xl opacity-70 dark:opacity-40 animate-blob"></div>
<div className="absolute top-0 right-1/4 w-72 h-72 bg-yellow-300 dark:bg-yellow-600 rounded-full mix-blend-multiply dark:mix-blend-screen filter blur-xl opacity-70 dark:opacity-40 animate-blob animation-delay-2000"></div>
<div className="absolute -bottom-8 left-1/3 w-72 h-72 bg-pink-300 dark:bg-pink-600 rounded-full mix-blend-multiply dark:mix-blend-screen filter blur-xl opacity-70 dark:opacity-40 animate-blob animation-delay-4000"></div>
<div className="fixed inset-0 z-0 h-full w-full bg-white dark:bg-neutral-950 overflow-hidden">
<div className="absolute inset-0 bg-[linear-gradient(to_right,#6366f11a_1px,transparent_1px),linear-gradient(to_bottom,#6366f11a_1px,transparent_1px)] bg-[size:24px_24px]"></div>
{/* Animated Blob */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[800px] bg-neutral-300 dark:bg-neutral-500 rounded-full mix-blend-multiply dark:mix-blend-screen filter blur-3xl opacity-40 dark:opacity-30 animate-blob"></div>
</div>
<Navbar />
<div className="container mx-auto px-4 pt-32 pb-12">
<div className="flex flex-col items-center justify-center gap-12">
<div className="text-center space-y-6 max-w-5xl">
<div className="relative z-10">
<Navbar />
<div className="container mx-auto px-4 pt-32 pb-8">
<div className="flex flex-col items-center justify-center gap-4">
<div className="text-center space-y-4 max-w-5xl">
<h1 className="text-5xl font-extrabold tracking-tight lg:text-7xl bg-clip-text text-transparent bg-gradient-to-b from-neutral-800 to-neutral-500 dark:from-neutral-100 dark:to-neutral-400">
PDF to Markdown
PDF to Markdown
</h1>
<p className="text-xl text-muted-foreground leading-relaxed">
Start extracting content from your documents in seconds. <br className="hidden sm:inline" />
Simply upload your PDF and get clean, formatted Markdown instantly.
Start extracting content from your documents in seconds. <br className="hidden sm:inline" />
Simply upload your PDF and get clean, formatted Markdown instantly.
</p>
</div>
<div className="w-full max-w-6xl mt-8">
</div>
<div className="w-full max-w-6xl mt-4">
<UploadForm />
</div>
</div>
</div>
</div>

View File

@@ -9,7 +9,7 @@ import { Textarea } from '~/components/ui/textarea';
import { Alert, AlertDescription, AlertTitle } from '~/components/ui/alert';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '~/components/ui/tabs';
import { convertPdf } from '~/app/actions';
import { Loader2, Upload, FileText, File as FileIcon, X } from 'lucide-react';
import { Loader2, Upload, FileText, File as FileIcon, X, Check, Clipboard, Sparkles, RotateCcw } from 'lucide-react';
export function UploadForm() {
const [isLoading, setIsLoading] = useState(false);
@@ -17,6 +17,7 @@ export function UploadForm() {
const [error, setError] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState<string>('upload');
const [fileName, setFileName] = useState<string | null>(null);
const [isCopied, setIsCopied] = useState(false);
async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
@@ -32,9 +33,9 @@ export function UploadForm() {
setMarkdown(result.data);
setActiveTab('output');
} else {
setError(result.error || 'An unknown error occurred');
setError(result.error ?? 'An unknown error occurred');
}
} catch (e) {
} catch {
setError('Failed to communicate with the server');
} finally {
setIsLoading(false);
@@ -63,26 +64,33 @@ export function UploadForm() {
return (
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full max-w-2xl mx-auto">
<TabsList className="grid w-full grid-cols-2 mb-8">
<TabsTrigger value="upload" className="flex items-center gap-2">
<TabsList className="grid w-full grid-cols-2 mb-4 h-auto p-1 bg-background/80 backdrop-blur-md border border-border/50 shadow-sm rounded-full">
<TabsTrigger
value="upload"
className="flex items-center gap-2 rounded-full data-[state=active]:bg-primary/10 data-[state=active]:text-primary data-[state=active]:shadow-none py-2.5 transition-all"
>
<Upload className="w-4 h-4" /> Upload
</TabsTrigger>
<TabsTrigger value="output" disabled={!markdown} className="flex items-center gap-2">
<TabsTrigger
value="output"
disabled={!markdown}
className="flex items-center gap-2 rounded-full data-[state=active]:bg-primary/10 data-[state=active]:text-primary data-[state=active]:shadow-none py-2.5 transition-all"
>
<FileText className="w-4 h-4" /> Output
</TabsTrigger>
</TabsList>
<TabsContent value="upload">
<Card>
<Card className="bg-background/60 backdrop-blur-xl border-border/50 shadow-sm rounded-3xl">
<CardHeader>
<CardTitle>Upload PDF</CardTitle>
<CardDescription>Select a PDF file to convert to Markdown</CardDescription>
</CardHeader>
<form onSubmit={onSubmit}>
<CardContent className="grid w-full items-center gap-4 py-8">
<CardContent className="grid w-full items-center gap-4 p-6">
<div className="flex flex-col space-y-4">
{!fileName ? (
<Label htmlFor="file" className="text-center cursor-pointer border-2 border-dashed border-muted-foreground/25 rounded-lg p-12 hover:bg-muted/50 transition-colors flex flex-col items-center gap-4">
<Label htmlFor="file" className="text-center cursor-pointer border-2 border-dashed border-muted-foreground/25 rounded-xl p-8 hover:bg-muted/50 transition-colors flex flex-col items-center gap-4">
<div className="p-4 rounded-full bg-primary/10 text-primary">
<Upload className="w-8 h-8" />
</div>
@@ -99,7 +107,7 @@ export function UploadForm() {
</div>
<span className="font-medium truncate max-w-[200px] sm:max-w-md">{fileName}</span>
</div>
<Button type="button" variant="ghost" size="icon" onClick={clearFile} aria-label="Remove file">
<Button type="button" variant="ghost" size="icon" onClick={clearFile} aria-label="Remove file" className="rounded-full">
<X className="w-4 h-4" />
</Button>
</div>
@@ -116,14 +124,22 @@ export function UploadForm() {
</div>
</CardContent>
<CardFooter className="flex justify-end">
<Button type="submit" disabled={isLoading || !fileName} className="w-full sm:w-auto">
<Button
type="submit"
variant="outline"
disabled={isLoading || !fileName}
className="w-full sm:w-auto rounded-xl border-2 hover:bg-muted/50"
>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Converting...
</>
) : (
'Convert to Markdown'
<>
<Sparkles className="mr-2 h-4 w-4" />
Convert to Markdown
</>
)}
</Button>
</CardFooter>
@@ -139,13 +155,16 @@ export function UploadForm() {
</TabsContent>
<TabsContent value="output">
<Card className="h-full">
<Card className="h-full bg-background/60 backdrop-blur-xl border-border/50 shadow-sm rounded-3xl">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle>Markdown Output</CardTitle>
<Button variant="outline" size="sm" onClick={() => {
void navigator.clipboard.writeText(markdown);
}}>
Copy to Clipboard
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
}} className="min-w-[40px] rounded-xl">
{isCopied ? <Check className="w-4 h-4" /> : <Clipboard className="w-4 h-4" />}
{!isCopied && <span className="ml-2">Copy</span>}
</Button>
</CardHeader>
<CardContent className="pt-4">
@@ -156,13 +175,18 @@ export function UploadForm() {
/>
</CardContent>
<CardFooter>
<Button variant="ghost" onClick={() => {
setActiveTab('upload');
setFileName(null);
// Optional: clear file input if we verified it works above
const input = document.getElementById('file') as HTMLInputElement;
if (input) input.value = '';
}} className="w-full">
<Button
variant="outline"
onClick={() => {
setActiveTab('upload');
setFileName(null);
// Optional: clear file input if we verified it works above
const input = document.getElementById('file') as HTMLInputElement;
if (input) input.value = '';
}}
className="w-full rounded-xl border-2 hover:bg-muted/50"
>
<RotateCcw className="mr-2 h-4 w-4" />
Convert another file
</Button>
</CardFooter>

View File

@@ -10,8 +10,10 @@ export async function performOcrOnPage(page: PDFPageProxy): Promise<string> {
// Render PDF page to canvas
await page.render({
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
canvasContext: context as any, // Type mismatch between node-canvas and DOM canvas
viewport: viewport,
canvas: null,
}).promise;
// Convert canvas to image buffer

View File

@@ -25,9 +25,8 @@
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
--radius: 1rem;
--sidebar: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;