- 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' && (
-
onRemove(file.id)}
- aria-label="Remove file"
- >
-
-
-
-
- )}
-
+ {/* Faint ruled lines */}
+
- {/* File preview / icon */}
-
- {file.preview ? (
- /* eslint-disable-next-line @next/next/no-img-element */
-
- ) : (
-
+ {/* Left margin line */}
+
+
+ {/* Content */}
+
+ {/* Header: category + remove */}
+
- .{file.extension}
+ {categoryLabel}
-
- )}
- {/* Progress overlay */}
- {file.status === 'converting' && (
-
-
-
- {Math.round(file.progress)}%
-
+ {file.status !== 'converting' && (
+
onRemove(file.id)}
+ aria-label="Remove file"
+ >
+
+
+
+
+ )}
- )}
- {/* Done overlay */}
- {file.status === 'done' && (
-
-
-
-
+ {/* Extension — big typewriter style */}
+
+ {file.preview ? (
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+
+
+ ) : (
+
+ .{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}
+
+
+
+
onSetFormat(file.id, e.target.value)}
+ className="flex-1 min-w-0 font-mono text-[11px] font-bold text-text-dark bg-transparent px-2 py-1 rounded-sm border border-dashed cursor-pointer hover:border-text-dark/20 focus:outline-none focus:border-pink/40 transition-all appearance-none"
+ style={{ borderColor: `${categoryColor}30` }}
+ >
+ {file.availableFormats.map((fmt) => (
+
+ .{fmt}
+
+ ))}
+
-
- )}
+ )}
- {/* 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}
-
-
-
-
-
onSetFormat(file.id, e.target.value)}
- className="select-arrow-warm flex-1 min-w-0 font-mono text-xs font-bold text-text-dark bg-white px-3 py-1.5 rounded-xl border border-border-soft cursor-pointer hover:border-border-med focus:outline-none focus:ring-2 focus:ring-pink/20 focus:border-pink/40 transition-all"
- style={{ borderColor: `${categoryColor}40` }}
- >
- {file.availableFormats.map((fmt) => (
-
- .{fmt}
-
- ))}
-
+ {/* 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
-
- )}
+
);
}