diff --git a/src/app/convert/page.tsx b/src/app/convert/page.tsx new file mode 100644 index 0000000..8882e5c --- /dev/null +++ b/src/app/convert/page.tsx @@ -0,0 +1,171 @@ +'use client'; + +import { AnimatePresence, motion } from 'framer-motion'; +import Link from 'next/link'; +import { DropZone } from '@/components/DropZone'; +import { FileCard } from '@/components/FileCard'; +import { useFileUpload } from '@/hooks/useFileUpload'; +import { useConversion } from '@/hooks/useConversion'; +import { formatFileSize } from '@/lib/utils'; + +export default function ConvertPage() { + const { + files, + isDragging, + inputRef, + handleDragEnter, + handleDragLeave, + handleDragOver, + handleDrop, + handleFileInput, + openFilePicker, + removeFile, + updateFile, + setTargetFormat, + clearAll, + } = useFileUpload(); + + const { + isConverting, + convertAll, + downloadFile, + downloadAllAsZip, + } = useConversion(updateFile); + + const hasFiles = files.length > 0; + const convertableCount = files.filter( + (f) => f.targetFormat && f.status !== 'done' && f.availableFormats.length > 0 + ).length; + const completedCount = files.filter((f) => f.status === 'done').length; + const totalSize = files.reduce((acc, f) => acc + f.size, 0); + + return ( +
+ {/* Atmospheric backgrounds */} +
+
+ + {/* Header */} +
+ +
+ T +
+ Transmute + + + v1.0 / client-side + +
+ + {/* Drop Zone */} + + + {/* File Grid */} + {hasFiles && ( + + + {files.map((file, index) => ( + + ))} + + + )} + + {/* Action Bar */} + {hasFiles && ( + +
+ + {files.length} file{files.length !== 1 ? 's' : ''} + +
+ + {formatFileSize(totalSize)} + + {completedCount > 0 && ( + <> +
+ + {completedCount} converted + + + )} +
+ +
+ + + {completedCount > 0 && ( + downloadAllAsZip(files)} + initial={{ opacity: 0, scale: 0.9 }} + animate={{ opacity: 1, scale: 1 }} + > + + + + Download ZIP + + )} + + convertAll(files)} + disabled={isConverting || convertableCount === 0} + > + {isConverting ? ( + <> + + + + Converting... + + ) : ( + <> + + + + Transmute {convertableCount > 0 ? `(${convertableCount})` : ''} + + )} + +
+ + )} +
+ ); +} diff --git a/src/app/globals.css b/src/app/globals.css index ee1dadd..3bfab9e 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,730 +1,105 @@ @import "tailwindcss"; /* ============================================ - TRANSMUTE — Dark Industrial Theme + TRANSMUTE — Playful Pastel Theme + Force light color-scheme so dark mode OS + doesn't override our cream backgrounds. ============================================ */ -:root { - --bg-primary: #09090b; - --bg-secondary: #111114; - --bg-tertiary: #18181c; - --bg-card: rgba(255, 255, 255, 0.025); - --bg-card-hover: rgba(255, 255, 255, 0.05); - - --border: rgba(255, 255, 255, 0.07); - --border-hover: rgba(255, 255, 255, 0.14); - - --text-primary: #f0f0f2; - --text-secondary: #7a7a85; - --text-muted: #45454d; - - --accent: #00f0ff; - --accent-glow: rgba(0, 240, 255, 0.15); - --accent-dim: rgba(0, 240, 255, 0.5); - - --cat-image: #10b981; - --cat-document: #0ea5e9; - --cat-audio: #8b5cf6; - --cat-video: #f43f5e; - --cat-data: #f59e0b; - - --radius-sm: 6px; - --radius-md: 10px; - --radius-lg: 16px; - --radius-xl: 20px; -} - @theme inline { - --color-background: var(--bg-primary); - --color-foreground: var(--text-primary); - --font-sans: var(--font-space-grotesk); + --color-bg-cream: #fef7f0; + --color-bg-warm: #fff5eb; + --color-bg-peach: #ffecd9; + --color-text-dark: #2d1f14; + --color-text-mid: #7a6552; + --color-text-light: #b8a08a; + --color-border-soft: rgba(180, 140, 100, 0.12); + --color-border-med: rgba(180, 140, 100, 0.25); + --color-pink: #f472b6; + --color-purple: #a78bfa; + --color-blue: #60a5fa; + --color-mint: #34d399; + --color-orange: #fb923c; + --color-teal: #2dd4bf; + + --font-sans: var(--font-plus-jakarta-sans); + --font-serif: var(--font-fraunces); --font-mono: var(--font-jetbrains-mono); } -/* ---- Base ---- */ -* { - box-sizing: border-box; +html { + color-scheme: light; } body { - background: var(--bg-primary); - color: var(--text-primary); - font-family: var(--font-space-grotesk), system-ui, sans-serif; + background: var(--color-bg-cream); + color: var(--color-text-dark); + font-family: var(--font-plus-jakarta-sans), system-ui, sans-serif; overflow-x: hidden; } -/* Noise texture overlay */ -.noise-overlay { - position: fixed; - inset: 0; - pointer-events: none; - z-index: 0; - opacity: 0.035; - background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); - background-repeat: repeat; - background-size: 256px 256px; -} - -/* Gradient mesh background */ -.bg-mesh { - position: fixed; - inset: 0; - pointer-events: none; - z-index: 0; - background: - radial-gradient(ellipse 60% 50% at 20% 0%, rgba(0, 240, 255, 0.04) 0%, transparent 60%), - radial-gradient(ellipse 50% 40% at 80% 100%, rgba(139, 92, 246, 0.03) 0%, transparent 60%); -} - /* ---- Scrollbar ---- */ -::-webkit-scrollbar { width: 6px; } +::-webkit-scrollbar { width: 8px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.1); - border-radius: 3px; + background: rgba(180, 140, 100, 0.15); + border-radius: 4px; } ::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.18); + background: rgba(180, 140, 100, 0.25); } -/* ============================================ - Header - ============================================ */ -.app-header { - position: sticky; - top: 0; - z-index: 50; - display: flex; - align-items: center; - justify-content: space-between; - padding: 16px 24px; - background: rgba(9, 9, 11, 0.8); - backdrop-filter: blur(12px); - border-bottom: 1px solid var(--border); +/* ---- Gradient text utility ---- */ +/* Removed — using solid text colors instead */ + +/* ---- Animations ---- */ +@keyframes badge-pulse { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.6; transform: scale(0.85); } } -.logo { - display: flex; - align-items: center; - gap: 10px; - font-family: var(--font-space-grotesk), sans-serif; - font-weight: 700; - font-size: 18px; - letter-spacing: -0.02em; - color: var(--text-primary); +@keyframes pulse-soft { + 0%, 100% { box-shadow: 0 4px 20px rgba(244, 114, 182, 0.25); } + 50% { box-shadow: 0 4px 32px rgba(167, 139, 250, 0.35); } } -.logo-icon { - width: 28px; - height: 28px; - border-radius: 7px; - background: linear-gradient(135deg, var(--accent), #0090ff); - display: flex; - align-items: center; - justify-content: center; - color: #000; - font-weight: 800; - font-size: 14px; +.animate-badge-pulse { + animation: badge-pulse 2s ease-in-out infinite; } -.header-tag { - font-family: var(--font-jetbrains-mono), monospace; - font-size: 11px; - color: var(--text-muted); - padding: 3px 8px; - border: 1px solid var(--border); - border-radius: 20px; - letter-spacing: 0.03em; +.animate-pulse-soft { + animation: pulse-soft 2s ease-in-out infinite; } -/* ============================================ - Drop Zone — Hero (no files) - ============================================ */ -.drop-zone-hero { - position: relative; - display: flex; - align-items: center; - justify-content: center; - min-height: calc(100vh - 57px); - padding: 40px 24px; +/* ---- Dot pattern background ---- */ +.bg-dots { + background-image: radial-gradient(circle, rgba(180, 140, 100, 0.12) 1px, transparent 1px); + background-size: 24px 24px; } -.drop-zone-inner { - position: relative; - width: 100%; - max-width: 640px; - padding: 64px 40px; - border: 1.5px dashed var(--border-hover); - border-radius: var(--radius-xl); - background: var(--bg-card); - transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1); - cursor: pointer; +/* ---- Pastel mesh background ---- */ +.bg-pastel-mesh { + background-color: rgba(244, 114, 182, 0.03); } -.drop-zone-inner.dragging { - border-color: var(--accent); - background: var(--accent-glow); - box-shadow: - 0 0 40px rgba(0, 240, 255, 0.08), - inset 0 0 40px rgba(0, 240, 255, 0.03); -} - -/* Corner accents */ -.corner-accent { - position: absolute; - width: 16px; - height: 16px; - border-color: var(--accent-dim); - transition: border-color 0.3s; -} - -.drop-zone-inner.dragging .corner-accent { - border-color: var(--accent); -} - -.corner-accent.top-left { - top: -1px; left: -1px; - border-top: 2px solid; - border-left: 2px solid; - border-top-left-radius: var(--radius-xl); -} -.corner-accent.top-right { - top: -1px; right: -1px; - border-top: 2px solid; - border-right: 2px solid; - border-top-right-radius: var(--radius-xl); -} -.corner-accent.bottom-left { - bottom: -1px; left: -1px; - border-bottom: 2px solid; - border-left: 2px solid; - border-bottom-left-radius: var(--radius-xl); -} -.corner-accent.bottom-right { - bottom: -1px; right: -1px; - border-bottom: 2px solid; - border-right: 2px solid; - border-bottom-right-radius: var(--radius-xl); -} - -.drop-zone-content { - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - gap: 16px; -} - -.upload-icon { - color: var(--accent); - margin-bottom: 8px; - filter: drop-shadow(0 0 12px rgba(0, 240, 255, 0.3)); -} - -.drop-zone-title { - font-family: var(--font-space-grotesk), sans-serif; - font-size: 28px; - font-weight: 700; - letter-spacing: -0.03em; - color: var(--text-primary); - line-height: 1.2; -} - -.drop-zone-subtitle { - font-size: 15px; - color: var(--text-secondary); - max-width: 380px; - line-height: 1.5; -} - -.browse-button { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 10px 24px; - margin-top: 8px; - font-family: var(--font-space-grotesk), sans-serif; - font-size: 14px; - font-weight: 600; - color: #000; - background: var(--accent); - border: none; - border-radius: var(--radius-md); - cursor: pointer; - transition: all 0.2s; - box-shadow: 0 0 20px rgba(0, 240, 255, 0.2); -} - -.browse-button:hover { - box-shadow: 0 0 30px rgba(0, 240, 255, 0.3); -} - -.drop-zone-hint { - font-family: var(--font-jetbrains-mono), monospace; - font-size: 11px; - color: var(--text-muted); - margin-top: 8px; - letter-spacing: 0.02em; -} - -/* ============================================ - Drop Zone — Compact (has files) - ============================================ */ -.drop-zone-compact { - padding: 16px 24px; - position: relative; - z-index: 1; -} - -.compact-drop-area { - display: flex; - align-items: center; - justify-content: center; - gap: 10px; - padding: 14px; - border: 1.5px dashed var(--border); - border-radius: var(--radius-md); - background: var(--bg-card); - cursor: pointer; - transition: all 0.3s; - color: var(--text-secondary); - font-size: 13px; - font-weight: 500; -} - -.compact-drop-area:hover { - border-color: var(--border-hover); - background: var(--bg-card-hover); - color: var(--text-primary); -} - -.compact-drop-area.dragging { - border-color: var(--accent); - background: var(--accent-glow); - color: var(--accent); -} - -/* ============================================ - File Cards - ============================================ */ -.file-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); - gap: 16px; - padding: 0 24px 120px; - position: relative; - z-index: 1; -} - -.file-card { - position: relative; - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius-lg); - padding: 16px; - display: flex; - flex-direction: column; - gap: 12px; - transition: all 0.25s ease; - overflow: hidden; -} - -.file-card:hover { - background: var(--bg-card-hover); - border-color: var(--border-hover); -} - -.card-accent-line { +/* ---- Top-border accent for privacy card ---- */ +.border-t-rainbow::before { + content: ''; position: absolute; top: 0; left: 0; right: 0; - height: 2px; - opacity: 0.6; + height: 3px; + background: var(--color-pink); + border-radius: inherit; } -.card-header { - display: flex; - align-items: center; - justify-content: space-between; -} - -.category-badge { - font-family: var(--font-jetbrains-mono), monospace; - font-size: 10px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.08em; - padding: 3px 8px; - border-radius: 20px; - border: 1px solid; -} - -.remove-btn { - width: 24px; - height: 24px; - display: flex; - align-items: center; - justify-content: center; - border: none; - background: transparent; - color: var(--text-muted); - cursor: pointer; - border-radius: 6px; - transition: all 0.2s; -} - -.remove-btn:hover { - color: #f43f5e; - background: rgba(244, 63, 94, 0.1); -} - -/* Preview area */ -.card-preview { - position: relative; - width: 100%; - height: 100px; - border-radius: var(--radius-sm); - background: var(--bg-secondary); - overflow: hidden; - display: flex; - align-items: center; - justify-content: center; -} - -.preview-image { - width: 100%; - height: 100%; - object-fit: cover; -} - -.preview-icon { - display: flex; - flex-direction: column; - align-items: center; - gap: 4px; -} - -.file-ext { - font-family: var(--font-jetbrains-mono), monospace; - font-size: 18px; - font-weight: 700; - opacity: 0.5; -} - -.progress-overlay, -.done-overlay, -.error-overlay { - position: absolute; - inset: 0; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 4px; - background: rgba(9, 9, 11, 0.75); - backdrop-filter: blur(4px); -} - -.progress-text { - font-family: var(--font-jetbrains-mono), monospace; - font-size: 11px; - color: var(--text-secondary); - margin-top: 2px; -} - -/* File info */ -.card-info { - display: flex; - flex-direction: column; - gap: 2px; -} - -.file-name { - font-size: 13px; - font-weight: 600; - color: var(--text-primary); - line-height: 1.3; - word-break: break-word; -} - -.file-size { - font-family: var(--font-jetbrains-mono), monospace; - font-size: 11px; - color: var(--text-muted); -} - -.error-message { - font-size: 11px; - color: #f43f5e; - background: rgba(244, 63, 94, 0.08); - padding: 6px 8px; - border-radius: var(--radius-sm); - line-height: 1.4; -} - -/* Format selector */ -.format-selector { - display: flex; - align-items: center; - gap: 8px; -} - -.format-from { - font-family: var(--font-jetbrains-mono), monospace; - font-size: 12px; - font-weight: 600; - color: var(--text-secondary); - padding: 4px 8px; - background: var(--bg-secondary); - border-radius: var(--radius-sm); -} - -.format-arrow { - color: var(--text-muted); - flex-shrink: 0; -} - -.format-select { - flex: 1; - font-family: var(--font-jetbrains-mono), monospace; - font-size: 12px; - font-weight: 600; - color: var(--accent); - background: var(--bg-secondary); - border: 1px solid; - border-radius: var(--radius-sm); - padding: 4px 8px; - cursor: pointer; - outline: none; +/* ---- Format select dropdown arrow ---- */ +.select-arrow-warm { appearance: none; -webkit-appearance: none; - background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%237a7a85' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); + background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%23b8a08a' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); background-repeat: no-repeat; - background-position: right 8px center; - padding-right: 24px; -} - -.format-select option { - background: var(--bg-secondary); - color: var(--text-primary); -} - -.download-btn { - display: flex; - align-items: center; - justify-content: center; - gap: 6px; - width: 100%; - padding: 8px; - font-family: var(--font-space-grotesk), sans-serif; - font-size: 12px; - font-weight: 600; - color: #000; - background: #10b981; - border: none; - border-radius: var(--radius-sm); - cursor: pointer; - transition: all 0.2s; -} - -.download-btn:hover { - background: #34d399; - box-shadow: 0 0 16px rgba(16, 185, 129, 0.3); -} - -.unsupported-msg { - font-size: 11px; - color: var(--text-muted); - text-align: center; - padding: 6px; - background: var(--bg-secondary); - border-radius: var(--radius-sm); -} - -/* ============================================ - Action Bar - ============================================ */ -.action-bar { - position: fixed; - bottom: 0; - left: 0; - right: 0; - z-index: 40; - display: flex; - align-items: center; - justify-content: space-between; - padding: 16px 24px; - background: rgba(9, 9, 11, 0.85); - backdrop-filter: blur(16px); - border-top: 1px solid var(--border); -} - -.action-bar-info { - display: flex; - align-items: center; - gap: 16px; -} - -.action-bar-stat { - font-family: var(--font-jetbrains-mono), monospace; - font-size: 12px; - color: var(--text-secondary); - display: flex; - align-items: center; - gap: 6px; -} - -.action-bar-stat strong { - color: var(--text-primary); - font-weight: 600; -} - -.stat-divider { - width: 1px; - height: 16px; - background: var(--border); -} - -.action-bar-buttons { - display: flex; - align-items: center; - gap: 10px; -} - -.btn-secondary { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 8px 16px; - font-family: var(--font-space-grotesk), sans-serif; - font-size: 13px; - font-weight: 600; - color: var(--text-secondary); - background: transparent; - border: 1px solid var(--border); - border-radius: var(--radius-md); - cursor: pointer; - transition: all 0.2s; -} - -.btn-secondary:hover { - color: var(--text-primary); - border-color: var(--border-hover); - background: var(--bg-card); -} - -.btn-convert { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 10px 28px; - font-family: var(--font-space-grotesk), sans-serif; - font-size: 14px; - font-weight: 700; - color: #000; - background: var(--accent); - border: none; - border-radius: var(--radius-md); - cursor: pointer; - transition: all 0.2s; - box-shadow: 0 0 24px rgba(0, 240, 255, 0.2); - letter-spacing: -0.01em; -} - -.btn-convert:hover:not(:disabled) { - box-shadow: 0 0 36px rgba(0, 240, 255, 0.35); - transform: translateY(-1px); -} - -.btn-convert:disabled { - opacity: 0.5; - cursor: not-allowed; - box-shadow: none; -} - -.btn-convert.converting { - background: var(--accent-dim); - animation: pulse-glow 2s ease-in-out infinite; -} - -@keyframes pulse-glow { - 0%, 100% { box-shadow: 0 0 24px rgba(0, 240, 255, 0.2); } - 50% { box-shadow: 0 0 40px rgba(0, 240, 255, 0.4); } -} - -.btn-download-all { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 10px 20px; - font-family: var(--font-space-grotesk), sans-serif; - font-size: 13px; - font-weight: 600; - color: #000; - background: #10b981; - border: none; - border-radius: var(--radius-md); - cursor: pointer; - transition: all 0.2s; - box-shadow: 0 0 16px rgba(16, 185, 129, 0.2); -} - -.btn-download-all:hover { - background: #34d399; - box-shadow: 0 0 24px rgba(16, 185, 129, 0.35); -} - -/* ============================================ - Responsive - ============================================ */ -@media (max-width: 640px) { - .file-grid { - grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); - gap: 12px; - padding: 0 16px 120px; - } - - .drop-zone-hero { - padding: 24px 16px; - min-height: calc(100vh - 57px); - } - - .drop-zone-inner { - padding: 48px 24px; - } - - .drop-zone-title { - font-size: 22px; - } - - .action-bar { - flex-direction: column; - gap: 12px; - padding: 12px 16px; - } - - .action-bar-buttons { - width: 100%; - } - - .btn-convert { - flex: 1; - } - - .app-header { - padding: 12px 16px; - } -} - -/* ============================================ - Utility animations - ============================================ */ -@keyframes fade-in { - from { opacity: 0; } - to { opacity: 1; } -} - -.animate-fade-in { - animation: fade-in 0.5s ease both; + background-position: right 10px center; + padding-right: 28px; } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 6c026ba..2cd88af 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,11 +1,17 @@ import type { Metadata } from "next"; -import { Space_Grotesk, JetBrains_Mono } from "next/font/google"; +import { Fraunces, Plus_Jakarta_Sans, JetBrains_Mono } from "next/font/google"; import "./globals.css"; -const spaceGrotesk = Space_Grotesk({ - variable: "--font-space-grotesk", +const fraunces = Fraunces({ + variable: "--font-fraunces", subsets: ["latin"], - weight: ["400", "500", "600", "700"], + weight: ["400", "500", "600", "700", "800", "900"], +}); + +const plusJakartaSans = Plus_Jakarta_Sans({ + variable: "--font-plus-jakarta-sans", + subsets: ["latin"], + weight: ["400", "500", "600", "700", "800"], }); const jetbrainsMono = JetBrains_Mono({ @@ -40,7 +46,7 @@ export default function RootLayout({ return ( {children} diff --git a/src/app/page.tsx b/src/app/page.tsx index 0fc3c89..4c4785e 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,167 +1,364 @@ 'use client'; -import { AnimatePresence, motion } from 'framer-motion'; -import { DropZone } from '@/components/DropZone'; -import { FileCard } from '@/components/FileCard'; -import { useFileUpload } from '@/hooks/useFileUpload'; -import { useConversion } from '@/hooks/useConversion'; -import { formatFileSize } from '@/lib/utils'; +import { motion } from 'framer-motion'; +import Link from 'next/link'; -export default function Home() { - const { - files, - isDragging, - inputRef, - handleDragEnter, - handleDragLeave, - handleDragOver, - handleDrop, - handleFileInput, - openFilePicker, - removeFile, - updateFile, - setTargetFormat, - clearAll, - } = useFileUpload(); +const floatingBadges = [ + { label: 'PNG', color: 'bg-pink-200', dot: 'bg-pink-400', top: '18%', left: '8%', delay: 0 }, + { label: 'MP4', color: 'bg-orange-100', dot: 'bg-orange-400', top: '22%', right: '10%', delay: 0.5 }, + { label: 'CSV', color: 'bg-emerald-100', dot: 'bg-emerald-400', bottom: '32%', left: '6%', delay: 1.0 }, + { label: 'PDF', color: 'bg-blue-100', dot: 'bg-blue-400', bottom: '28%', right: '8%', delay: 0.3 }, + { label: 'WAV', color: 'bg-purple-100', dot: 'bg-purple-400', top: '42%', left: '3%', delay: 0.7 }, + { label: 'WEBP', color: 'bg-pink-100', dot: 'bg-pink-400', top: '35%', right: '4%', delay: 1.2 }, +]; - const { - isConverting, - convertAll, - downloadFile, - downloadAllAsZip, - } = useConversion(updateFile); +const features = [ + { + icon: '\u{1F5BC}', + title: 'Images', + desc: 'PNG, JPG, WebP, GIF, BMP, AVIF, SVG \u2014 convert between any format using Canvas API.', + bg: 'bg-pink-50', + iconBg: 'bg-pink-100', + formats: ['PNG', 'JPG', 'WebP', 'GIF', 'AVIF', 'SVG'], + wide: true, + }, + { + icon: '\u{1F4C4}', + title: 'Documents', + desc: 'DOCX, PDF, Markdown, HTML, TXT \u2014 preserves formatting with styled rendering.', + bg: 'bg-blue-50', + iconBg: 'bg-blue-100', + formats: ['DOCX', 'PDF', 'MD', 'HTML', 'TXT'], + wide: false, + }, + { + icon: '\u{1F3B5}', + title: 'Audio', + desc: 'MP3, WAV, OGG, AAC, FLAC, M4A \u2014 powered by FFmpeg WebAssembly.', + bg: 'bg-purple-50', + iconBg: 'bg-purple-100', + formats: ['MP3', 'WAV', 'OGG', 'FLAC'], + wide: false, + }, + { + icon: '\u{1F3AC}', + title: 'Video', + desc: 'MP4, WebM, AVI, MOV, MKV \u2014 full video transcoding in your browser.', + bg: 'bg-orange-50', + iconBg: 'bg-orange-100', + formats: ['MP4', 'WebM', 'AVI', 'MOV'], + wide: false, + }, + { + icon: '\u{1F4CA}', + title: 'Data', + desc: 'CSV, JSON, XML, YAML, TSV \u2014 smart parsing with structure preservation.', + bg: 'bg-emerald-50', + iconBg: 'bg-emerald-100', + formats: ['CSV', 'JSON', 'XML', 'YAML', 'TSV'], + wide: true, + }, +]; - const hasFiles = files.length > 0; - const convertableCount = files.filter( - (f) => f.targetFormat && f.status !== 'done' && f.availableFormats.length > 0 - ).length; - const completedCount = files.filter((f) => f.status === 'done').length; - const totalSize = files.reduce((acc, f) => acc + f.size, 0); +const stagger = { + hidden: {}, + visible: { transition: { staggerChildren: 0.08 } }, +}; +const fadeUp = { + hidden: { opacity: 0, y: 24 }, + visible: { + opacity: 1, + y: 0, + transition: { duration: 0.6, ease: [0.16, 1, 0.3, 1] as const }, + }, +}; + +export default function LandingPage() { return ( -
+
{/* Atmospheric backgrounds */} -
-
+
+
- {/* Header */} -
-
-
T
- Transmute -
- v1.0 / client-side + {/* ──── NAV ──── */} +
+ +
+ T +
+ Transmute + + + Open Converter +
- {/* Drop Zone */} - + {/* ──── HERO ──── */} +
+ {/* Floating format badges */} + {floatingBadges.map((badge) => ( + +
+ + .{badge.label} +
+
+ ))} - {/* File Grid */} - {hasFiles && ( - - {files.map((file, index) => ( - - ))} - - - )} + {/* Badge */} + + + 100% client-side, forever free + - {/* Action Bar */} - {hasFiles && ( - -
- - {files.length} file{files.length !== 1 ? 's' : ''} - -
- - {formatFileSize(totalSize)} - - {completedCount > 0 && ( - <> -
- - {completedCount} converted - - - )} -
+ {/* Title */} + + Convert anything to{' '} + everything + -
- + {/* Subtitle */} + + Drop images, docs, audio, video, or data files — get instant conversions + right in your browser. No uploads. No accounts. No limits. + - {completedCount > 0 && ( - downloadAllAsZip(files)} - initial={{ opacity: 0, scale: 0.9 }} - animate={{ opacity: 1, scale: 1 }} - whileHover={{ scale: 1.03 }} - whileTap={{ scale: 0.97 }} - > - - - - Download ZIP - - )} - - convertAll(files)} - disabled={isConverting || convertableCount === 0} - whileHover={!isConverting && convertableCount > 0 ? { scale: 1.03 } : {}} - whileTap={!isConverting && convertableCount > 0 ? { scale: 0.97 } : {}} + {/* CTAs */} + + - {isConverting ? ( - <> - - + + + + Start Converting + + + See what's possible + + + + + + +
+ + {/* ──── FEATURES ──── */} +
+ + + Formats + +

+ Every format you need +

+

+ 40+ file formats across 5 categories, all converted instantly with zero quality loss. +

+
+ + + {features.map((feat) => ( + +
+ {feat.icon} +
+

{feat.title}

+

{feat.desc}

+
+ {feat.formats.map((f) => ( + + .{f} + + ))} +
+
+ ))} +
+
+ + {/* ──── HOW IT WORKS ──── */} +
+ + + How it works + +

+ Three steps. That's it. +

+
+ + + {[ + { num: '1', title: 'Drop your files', desc: 'Drag and drop any file \u2014 or click to browse. We accept everything.' }, + { num: '2', title: 'Pick a format', desc: 'Choose your target format from smart suggestions based on file type.' }, + { num: '3', title: 'Download', desc: 'Hit convert and download instantly. Files never leave your browser.' }, + ].map((step, i) => ( + +
+ {step.num} +
+

{step.title}

+

{step.desc}

+ {i < 2 && ( +
+ + - Converting... - - ) : ( - <> - - - - Transmute {convertableCount > 0 ? `(${convertableCount})` : ''} - +
)} - +
+ ))} +
+
+ + {/* ──── PRIVACY ──── */} +
+ +
+ + + +
+

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. +

+
+ {[ + { label: 'No uploads', color: 'bg-emerald-50', stroke: '#34d399' }, + { label: 'No servers', color: 'bg-blue-50', stroke: '#60a5fa' }, + { label: 'No tracking', color: 'bg-purple-50', stroke: '#a78bfa' }, + { label: 'No limits', color: 'bg-orange-50', stroke: '#fb923c' }, + ].map((b) => ( +
+
+ + + +
+ {b.label} +
+ ))}
- )} +
+ + {/* ──── FOOTER CTA ──── */} +
+ + Ready to transmute? + + + + + + + Open Converter + + +
+ + {/* ──── FOOTER ──── */} +
+

+ Built with love, runs on your machine. +

+
); } diff --git a/src/components/DropZone.tsx b/src/components/DropZone.tsx index 9d2dcd6..154d1e0 100644 --- a/src/components/DropZone.tsx +++ b/src/components/DropZone.tsx @@ -26,10 +26,11 @@ export function DropZone({ onFileInput, onBrowse, }: DropZoneProps) { + // Compact mode — thin bar when files are present if (hasFiles) { return (
- + - Drop more files or click to browse + + {isDragging ? 'Release to add files' : 'Drop more files or click to browse'} +
); } + // Hero mode — full drop zone when no files return (
- {/* Animated corner accents */} -
-
-
-
+ {/* Corner accents */} +
+
+
+
{/* Upload icon */} - + -

+

{isDragging ? 'Release to transmute' : 'Drop anything.'}

-

+

{isDragging ? 'Your files are ready for transformation' : 'Images, documents, audio, video, data \u2014 all formats welcome'}

-

- 100% client-side \u2014 your files never leave your browser +

+ 100% client-side — your files never leave your browser

diff --git a/src/components/FileCard.tsx b/src/components/FileCard.tsx index ed44acb..c8044e7 100644 --- a/src/components/FileCard.tsx +++ b/src/components/FileCard.tsx @@ -25,30 +25,27 @@ export function FileCard({ return ( {/* Top accent line */}
{/* Header: category badge + remove */} -
+
onRemove(file.id)} aria-label="Remove file" > @@ -72,80 +69,94 @@ export function FileCard({
{/* File preview / icon */} -
+
{file.preview ? ( /* eslint-disable-next-line @next/next/no-img-element */ {file.name} ) : ( -
- .{file.extension} +
+ + .{file.extension} +
)} {/* Progress overlay */} {file.status === 'converting' && ( -
+
- {Math.round(file.progress)}% + + {Math.round(file.progress)}% +
)} {/* Done overlay */} {file.status === 'done' && ( - - - +
+ + + +
)} {/* Error overlay */} {file.status === 'error' && ( -
- - - - +
+
+ + + + +
)}
{/* File info */} -
-

+

+

{truncateFilename(file.name)}

-

{formatFileSize(file.size)}

+

+ {formatFileSize(file.size)} +

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

{file.error}

+

+ {file.error} +

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