diff --git a/src/app/convert/page.tsx b/src/app/convert/page.tsx index 67670dd..52baf8a 100644 --- a/src/app/convert/page.tsx +++ b/src/app/convert/page.tsx @@ -77,7 +77,7 @@ export default function ConvertPage() { {/* File Grid */} {hasFiles && ( - {/* ──── FEATURES ──── */} + {/* ──── FEATURES — BENTO GRID ──── */}
- - {features.map((feat) => ( + {bentoFeatures.map((feat, i) => ( -
- {feat.icon} -
-

{feat.title}

-

{feat.desc}

-
- {feat.formats.map((f) => ( - + +
+ {/* Header row */} +
+
- .{f} - - ))} + {feat.icon} +
+
+

{feat.title}

+

{feat.desc}

+
+
+ + {/* Format badges — grow to fill space */} +
+ {feat.formats.map((f) => ( + + .{f} + + ))} +
))} - +
+ + {/* Total count callout */} + + 70+ formats supported — and counting +
{/* ──── HOW IT WORKS ──── */} -
+
- - {[ - { num: '1', icon: '\u{1F4E5}', title: 'Drop your files', desc: 'Drag and drop any file \u2014 or click to browse. We accept everything.' }, - { num: '2', icon: '\u{2699}', title: 'Pick a format', desc: 'Choose your target format from smart suggestions based on file type.' }, - { num: '3', icon: '\u{2B07}', title: 'Download', desc: 'Hit convert and download instantly. Files never leave your browser.' }, - ].map((step, i) => ( + {/* Timeline layout */} +
+ {/* Connecting line — desktop only */} +
+ +
+ {/* Step 1 — Drop */} -
{step.icon}
-
- {step.num} -
-

{step.title}

-

{step.desc}

- {i < 2 && ( -
- - - + {/* Visual scene */} +
+ {/* Background shape */} +
+
+ {/* File stack */} +
+
+
+
+ + + +
+
- )} +
+ {/* Number + text */} +
+ 1 +
+

Drop your files

+

+ Drag and drop anything {'\u2014'} images, docs, audio, video, data. We handle 70+ formats. +

- ))} - + + {/* Step 2 — Pick */} + + {/* Visual scene */} +
+
+
+ {/* Format picker mini-UI */} +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Number + text */} +
+ 2 +
+

Pick a format

+

+ Smart suggestions based on your file type. Or choose any compatible output format. +

+ + + {/* Step 3 — Download */} + + {/* Visual scene */} +
+
+
+ {/* Checkmark + download visual */} +
+
+ + + +
+ {/* Tiny sparkles */} +
+
+
+
+
+ {/* Number + text */} +
+ 3 +
+

Download

+

+ Converted instantly in your browser. Hit download {'\u2014'} done. Files never leave your machine. +

+ +
+
{/* ──── PRIVACY ──── */} -
+
-
{'\u{1F6E1}'}
-

Your files stay yours

-

- Every conversion happens entirely in your browser using WebAssembly and Canvas APIs. - No file ever touches a server. No data is collected. No account needed. Ever. -

-
- {[ - { icon: '\u{1F6AB}', label: 'No uploads', color: 'bg-emerald-50' }, - { icon: '\u{1F4BB}', label: 'No servers', color: 'bg-blue-50' }, - { icon: '\u{1F440}', label: 'No tracking', color: 'bg-purple-50' }, - { icon: '\u{267E}', label: 'No limits', color: 'bg-orange-50' }, - ].map((b) => ( -
-
- {b.icon} + {/* Left — Shield visual */} +
+ {/* Pulsing rings */} + + + {/* Shield body */} +
+ + + + +
+
+ + {/* Right — Text + privacy points */} +
+ + Privacy First + +

+ Your files stay yours +

+

+ Every conversion happens entirely in your browser using WebAssembly and Canvas APIs. + No file ever touches a server. No data is collected. No account needed. +

+ + {/* Privacy points — stacked vertically */} +
+ {[ + { icon: '\u{1F512}', label: 'No uploads', desc: 'Files stay on your device', accent: '#34d399' }, + { icon: '\u{1F6AB}', label: 'No servers', desc: 'Zero network requests', accent: '#60a5fa' }, + { icon: '\u{1F440}', label: 'No tracking', desc: 'No analytics or cookies', accent: '#a78bfa' }, + { icon: '\u{267E}\uFE0F', label: 'No limits', desc: 'Unlimited file size & count', accent: '#fb923c' }, + ].map((point) => ( +
+
+ {point.icon} +
+
+
{point.label}
+
{point.desc}
+
- {b.label} -
- ))} + ))} +
diff --git a/src/components/FileCard.tsx b/src/components/FileCard.tsx index 229ddd9..1ef86c3 100644 --- a/src/components/FileCard.tsx +++ b/src/components/FileCard.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useMemo } from 'react'; import { motion } from 'framer-motion'; import { UploadedFile, CATEGORY_COLORS, CATEGORY_LABELS } from '@/types'; import { formatFileSize, truncateFilename } from '@/lib/utils'; @@ -14,6 +15,15 @@ interface FileCardProps { onPreview: (file: UploadedFile) => void; } +/* Seeded random for consistent per-card rotation */ +function seededRandom(seed: string) { + let h = 0; + for (let i = 0; i < seed.length; i++) { + h = Math.imul(31, h) + seed.charCodeAt(i) | 0; + } + return ((h >>> 0) % 1000) / 1000; +} + export function FileCard({ file, index, @@ -25,190 +35,240 @@ export function FileCard({ const categoryColor = CATEGORY_COLORS[file.category]; const categoryLabel = CATEGORY_LABELS[file.category]; + // Stable random rotation per card (-2.5 to 2.5 degrees) + const rotation = useMemo(() => { + const r = seededRandom(file.id); + return (r - 0.5) * 5; + }, [file.id]); + + // Slight random tape offset + const tapeOffset = useMemo(() => { + const r = seededRandom(file.id + 'tape'); + return (r - 0.5) * 20; // -10 to 10px + }, [file.id]); + return ( - {/* Top accent line */} + {/* Paper shadow — slightly offset for depth */}
- {/* Header: category badge + remove */} -
- + {/* Tape strip across top */} +
- {categoryLabel} - + /> - {file.status !== 'converting' && ( - - )} -
+ {/* Faint ruled lines */} +
- {/* File preview / icon */} -
- {file.preview ? ( - /* eslint-disable-next-line @next/next/no-img-element */ - {file.name} - ) : ( -
+ {/* Left margin line */} +
+ + {/* Content */} +
+ {/* Header: category + remove */} +
- .{file.extension} + {categoryLabel} -
- )} - {/* Progress overlay */} - {file.status === 'converting' && ( -
- - - {Math.round(file.progress)}% - + {file.status !== 'converting' && ( + + )}
- )} - {/* Done overlay */} - {file.status === 'done' && ( - -
- - + {/* Extension — big typewriter style */} +
+ {file.preview ? ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {file.name} +
+ ) : ( + + .{file.extension} + + )} + + {/* Progress overlay */} + {file.status === 'converting' && ( +
+ + + {Math.round(file.progress)}% + +
+ )} + + {/* Done overlay */} + {file.status === 'done' && ( + +
+ + + +
+
+ )} + + {/* Error overlay */} + {file.status === 'error' && ( +
+
+ + + + +
+
+ )} +
+ + {/* Filename + size — handwritten feel area */} +
+

+ {truncateFilename(file.name)} +

+

+ {formatFileSize(file.size)} +

+
+ + {/* Error message */} + {file.status === 'error' && file.error && ( +

+ {file.error} +

+ )} + + {/* Format selector — styled like a form field on paper */} + {file.availableFormats.length > 0 && file.status !== 'done' && ( +
+ + .{file.extension} + + + +
- - )} + )} - {/* Error overlay */} - {file.status === 'error' && ( -
-
- - - - + {/* Action buttons — done state */} + {file.status === 'done' && ( +
+ onPreview(file)} + initial={{ opacity: 0, y: 4 }} + animate={{ opacity: 1, y: 0 }} + whileTap={{ scale: 0.97 }} + > + + + + + Preview + + onDownload(file)} + initial={{ opacity: 0, y: 4 }} + animate={{ opacity: 1, y: 0 }} + whileTap={{ scale: 0.97 }} + > + + + + .{file.targetFormat} +
-
- )} -
+ )} - {/* File info */} -
-

- {truncateFilename(file.name)} -

-

- {formatFileSize(file.size)} -

-
- - {/* Error message */} - {file.status === 'error' && file.error && ( -

- {file.error} -

- )} - - {/* Format selector */} - {file.availableFormats.length > 0 && file.status !== 'done' && ( -
- - .{file.extension} - - - - - + {/* Unsupported message */} + {file.availableFormats.length === 0 && ( +

+ Format not supported for conversion +

+ )}
- )} - - {/* Action buttons */} - {file.status === 'done' && ( -
- onPreview(file)} - initial={{ opacity: 0, y: 5 }} - animate={{ opacity: 1, y: 0 }} - whileHover={{ scale: 1.02 }} - whileTap={{ scale: 0.98 }} - > - - - - - Preview - - onDownload(file)} - initial={{ opacity: 0, y: 5 }} - animate={{ opacity: 1, y: 0 }} - whileHover={{ scale: 1.02 }} - whileTap={{ scale: 0.98 }} - > - - - - .{file.targetFormat} - -
- )} - - {/* Unsupported message */} - {file.availableFormats.length === 0 && ( -

- Format not supported for conversion -

- )} +
); }