redesign: playful pastel theme with solid colors, new landing page

- Rewrite all components to use Tailwind utility classes (Tailwind v4 broke custom CSS classes)
- New landing page at / with hero, features bento grid, how-it-works, privacy section
- Converter tool moved to /convert route
- Fonts: Fraunces (serif headlines) + Plus Jakarta Sans (body) + JetBrains Mono (labels)
- Warm cream palette with pastel category colors (pink, blue, purple, orange, mint)
- Remove all gradients — solid colors only throughout
- Remove floating badge bounce animation
- Responsive action bar and file grid for mobile
- Register design tokens via @theme inline for Tailwind v4 compatibility
This commit is contained in:
noah
2026-03-09 18:38:35 +01:00
parent 221a72b2bf
commit 38f58327c4
8 changed files with 690 additions and 911 deletions
+38 -21
View File
@@ -26,10 +26,11 @@ export function DropZone({
onFileInput,
onBrowse,
}: DropZoneProps) {
// Compact mode — thin bar when files are present
if (hasFiles) {
return (
<div
className="drop-zone-compact"
className="px-6 py-3 relative z-10"
onDragEnter={onDragEnter}
onDragLeave={onDragLeave}
onDragOver={onDragOver}
@@ -43,21 +44,28 @@ export function DropZone({
className="hidden"
/>
<div
className={`compact-drop-area ${isDragging ? 'dragging' : ''}`}
className={`flex items-center justify-center gap-2.5 px-5 py-3 rounded-2xl border-2 border-dashed cursor-pointer transition-all duration-200 select-none ${
isDragging
? 'border-pink bg-pink/5 text-pink scale-[1.01]'
: 'border-border-soft bg-white/60 text-text-light hover:border-pink/40 hover:text-pink/70 hover:bg-white/80'
}`}
onClick={onBrowse}
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
<path d="M12 5v14M5 12h14" />
</svg>
<span>Drop more files or click to browse</span>
<span className="text-sm font-semibold">
{isDragging ? 'Release to add files' : 'Drop more files or click to browse'}
</span>
</div>
</div>
);
}
// Hero mode — full drop zone when no files
return (
<div
className="drop-zone-hero"
className="flex items-center justify-center min-h-[70vh] px-6 py-16 relative z-10"
onDragEnter={onDragEnter}
onDragLeave={onDragLeave}
onDragOver={onDragOver}
@@ -72,36 +80,45 @@ export function DropZone({
/>
<motion.div
className={`drop-zone-inner ${isDragging ? 'dragging' : ''}`}
className={`relative w-full max-w-xl rounded-3xl border-2 border-dashed p-12 text-center transition-colors duration-300 ${
isDragging
? 'border-pink bg-pink/[0.04] shadow-[0_8px_40px_rgba(244,114,182,0.12)]'
: 'border-border-med bg-white/50 shadow-[0_4px_24px_rgba(180,140,100,0.06)]'
}`}
initial={{ opacity: 0, scale: 0.95 }}
animate={{
opacity: 1,
scale: isDragging ? 1.02 : 1,
}}
transition={{ duration: 0.4, ease: [0.16, 1, 0.3, 1] }}
transition={{ duration: 0.4, ease: [0.16, 1, 0.3, 1] as const }}
>
{/* Animated corner accents */}
<div className="corner-accent top-left" />
<div className="corner-accent top-right" />
<div className="corner-accent bottom-left" />
<div className="corner-accent bottom-right" />
{/* Corner accents */}
<div className="absolute top-3 left-3 w-5 h-5 border-t-2 border-l-2 border-pink/30 rounded-tl-lg" />
<div className="absolute top-3 right-3 w-5 h-5 border-t-2 border-r-2 border-purple/30 rounded-tr-lg" />
<div className="absolute bottom-3 left-3 w-5 h-5 border-b-2 border-l-2 border-blue/30 rounded-bl-lg" />
<div className="absolute bottom-3 right-3 w-5 h-5 border-b-2 border-r-2 border-mint/30 rounded-br-lg" />
<motion.div
className="drop-zone-content"
className="flex flex-col items-center gap-5"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2, duration: 0.6 }}
>
{/* Upload icon */}
<motion.div
className="upload-icon"
className={`flex items-center justify-center w-20 h-20 rounded-2xl transition-colors duration-300 ${
isDragging
? 'bg-pink/15 text-pink'
: 'bg-pink/8 text-pink/70'
}`}
animate={{
y: isDragging ? -8 : 0,
y: isDragging ? -10 : 0,
scale: isDragging ? 1.15 : 1,
rotate: isDragging ? -5 : 0,
}}
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
>
<svg width="48" height="48" viewBox="0 0 48 48" fill="none">
<svg width="40" height="40" viewBox="0 0 48 48" fill="none">
<path
d="M24 32V8M24 8L16 16M24 8L32 16"
stroke="currentColor"
@@ -119,17 +136,17 @@ export function DropZone({
</svg>
</motion.div>
<h2 className="drop-zone-title">
<h2 className="font-serif text-3xl font-extrabold text-text-dark tracking-tight">
{isDragging ? 'Release to transmute' : 'Drop anything.'}
</h2>
<p className="drop-zone-subtitle">
<p className="text-text-mid text-[15px] max-w-xs leading-relaxed">
{isDragging
? 'Your files are ready for transformation'
: 'Images, documents, audio, video, data \u2014 all formats welcome'}
</p>
<motion.button
className="browse-button"
className="inline-flex items-center gap-2 mt-2 px-7 py-3 text-sm font-bold text-white bg-pink rounded-2xl cursor-pointer shadow-[0_4px_20px_rgba(244,114,182,0.25)] hover:-translate-y-0.5 hover:shadow-[0_6px_28px_rgba(244,114,182,0.35)] transition-all border-none"
onClick={onBrowse}
whileHover={{ scale: 1.04 }}
whileTap={{ scale: 0.97 }}
@@ -140,8 +157,8 @@ export function DropZone({
Browse files
</motion.button>
<p className="drop-zone-hint">
100% client-side \u2014 your files never leave your browser
<p className="font-mono text-[11px] text-text-light tracking-wide mt-1">
100% client-side &mdash; your files never leave your browser
</p>
</motion.div>
</motion.div>
+62 -49
View File
@@ -25,30 +25,27 @@ export function FileCard({
return (
<motion.div
className="file-card"
className="relative bg-white rounded-2xl overflow-hidden border border-border-soft shadow-[0_2px_12px_rgba(180,140,100,0.06)] hover:shadow-[0_4px_24px_rgba(180,140,100,0.1)] hover:-translate-y-0.5 transition-all duration-200"
initial={{ opacity: 0, y: 20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -10, scale: 0.95 }}
transition={{
duration: 0.4,
delay: index * 0.05,
ease: [0.16, 1, 0.3, 1],
ease: [0.16, 1, 0.3, 1] as const,
}}
layout
style={{
'--card-accent': categoryColor,
} as React.CSSProperties}
>
{/* Top accent line */}
<div
className="card-accent-line"
className="h-[3px] w-full"
style={{ background: categoryColor }}
/>
{/* Header: category badge + remove */}
<div className="card-header">
<div className="flex items-center justify-between px-4 pt-3 pb-1">
<span
className="category-badge"
className="inline-flex items-center px-2.5 py-0.5 text-[11px] font-bold font-mono tracking-wider uppercase rounded-full border"
style={{
background: `${categoryColor}18`,
color: categoryColor,
@@ -60,7 +57,7 @@ export function FileCard({
{file.status !== 'converting' && (
<button
className="remove-btn"
className="flex items-center justify-center w-7 h-7 rounded-lg bg-transparent border-none cursor-pointer text-text-light hover:text-text-dark hover:bg-bg-warm transition-all"
onClick={() => onRemove(file.id)}
aria-label="Remove file"
>
@@ -72,80 +69,94 @@ export function FileCard({
</div>
{/* File preview / icon */}
<div className="card-preview">
<div className="relative flex items-center justify-center h-32 mx-4 mt-1 mb-2 rounded-xl bg-bg-warm/60 overflow-hidden">
{file.preview ? (
/* eslint-disable-next-line @next/next/no-img-element */
<img
src={file.preview}
alt={file.name}
className="preview-image"
className="w-full h-full object-cover"
/>
) : (
<div
className="preview-icon"
style={{ color: categoryColor }}
>
<span className="file-ext">.{file.extension}</span>
<div className="flex items-center justify-center w-full h-full">
<span
className="font-mono text-2xl font-black tracking-wider opacity-60"
style={{ color: categoryColor }}
>
.{file.extension}
</span>
</div>
)}
{/* Progress overlay */}
{file.status === 'converting' && (
<div className="progress-overlay">
<div className="absolute inset-0 flex flex-col items-center justify-center gap-1.5 bg-white/80 backdrop-blur-sm rounded-xl">
<ProgressRing progress={file.progress} color={categoryColor} />
<span className="progress-text">{Math.round(file.progress)}%</span>
<span className="font-mono text-xs font-bold text-text-dark">
{Math.round(file.progress)}%
</span>
</div>
)}
{/* Done overlay */}
{file.status === 'done' && (
<motion.div
className="done-overlay"
className="absolute inset-0 flex items-center justify-center bg-mint/10 backdrop-blur-sm rounded-xl"
initial={{ opacity: 0, scale: 0.5 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ type: 'spring', stiffness: 400, damping: 15 }}
>
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#10b981" strokeWidth="2.5">
<path d="M20 6L9 17l-5-5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
<div className="flex items-center justify-center w-12 h-12 rounded-full bg-white shadow-[0_2px_12px_rgba(52,211,153,0.2)]">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#34d399" strokeWidth="2.5">
<path d="M20 6L9 17l-5-5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
</motion.div>
)}
{/* Error overlay */}
{file.status === 'error' && (
<div className="error-overlay">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#f43f5e" strokeWidth="2">
<circle cx="12" cy="12" r="10" />
<path d="M12 8v4M12 16h.01" />
</svg>
<div className="absolute inset-0 flex items-center justify-center bg-red-50/80 backdrop-blur-sm rounded-xl">
<div className="flex items-center justify-center w-12 h-12 rounded-full bg-white shadow-[0_2px_12px_rgba(244,63,94,0.15)]">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#f43f5e" strokeWidth="2">
<circle cx="12" cy="12" r="10" />
<path d="M12 8v4M12 16h.01" />
</svg>
</div>
</div>
)}
</div>
{/* File info */}
<div className="card-info">
<p className="file-name" title={file.name}>
<div className="px-4 pb-1">
<p className="text-sm font-semibold text-text-dark truncate leading-snug" title={file.name}>
{truncateFilename(file.name)}
</p>
<p className="file-size">{formatFileSize(file.size)}</p>
<p className="font-mono text-[11px] text-text-light mt-0.5">
{formatFileSize(file.size)}
</p>
</div>
{/* Error message */}
{file.status === 'error' && file.error && (
<p className="error-message">{file.error}</p>
<p className="px-4 pb-2 text-[12px] text-red-400 leading-snug">
{file.error}
</p>
)}
{/* Format selector */}
{file.availableFormats.length > 0 && file.status !== 'done' && (
<div className="format-selector">
<span className="format-from">.{file.extension}</span>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="format-arrow">
<div className="flex items-center gap-2 px-4 pb-4 pt-1.5">
<span className="font-mono text-xs font-bold text-text-mid bg-bg-warm px-2 py-1 rounded-lg">
.{file.extension}
</span>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-text-light flex-shrink-0">
<path d="M5 12h14M12 5l7 7-7 7" />
</svg>
<select
value={file.targetFormat || ''}
onChange={(e) => onSetFormat(file.id, e.target.value)}
className="format-select"
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) => (
@@ -159,24 +170,26 @@ export function FileCard({
{/* Download button */}
{file.status === 'done' && (
<motion.button
className="download-btn"
onClick={() => onDownload(file)}
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3" />
</svg>
Download .{file.targetFormat}
</motion.button>
<div className="px-4 pb-4 pt-1">
<motion.button
className="w-full inline-flex items-center justify-center gap-2 px-4 py-2.5 text-[13px] font-bold text-white bg-mint border-none rounded-xl cursor-pointer shadow-[0_2px_12px_rgba(52,211,153,0.2)] hover:-translate-y-0.5 hover:shadow-[0_4px_20px_rgba(52,211,153,0.3)] transition-all"
onClick={() => onDownload(file)}
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3" />
</svg>
Download .{file.targetFormat}
</motion.button>
</div>
)}
{/* Unsupported message */}
{file.availableFormats.length === 0 && (
<p className="unsupported-msg">
<p className="px-4 pb-4 pt-1 text-[12px] text-text-light italic text-center">
Format not supported for conversion
</p>
)}
+2 -2
View File
@@ -11,7 +11,7 @@ export function ProgressRing({
progress,
size = 36,
strokeWidth = 3,
color = '#00F0FF',
color = '#f472b6',
}: ProgressRingProps) {
const radius = (size - strokeWidth) / 2;
const circumference = 2 * Math.PI * radius;
@@ -30,7 +30,7 @@ export function ProgressRing({
cy={size / 2}
r={radius}
fill="none"
stroke="rgba(255,255,255,0.08)"
stroke="rgba(0,0,0,0.06)"
strokeWidth={strokeWidth}
/>
{/* Progress ring */}