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:
+38
-21
@@ -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 — your files never leave your browser
|
||||
</p>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
+62
-49
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user