mirror of
https://github.com/soconnor0919/pdf2md.git
synced 2026-02-05 00:06:39 -05:00
feat: Revamp UI with new background and component styling, and add copy-to-clipboard and reset options.
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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%;
|
||||
|
||||
Reference in New Issue
Block a user