commit 1771ca42eb46045893351f6d8406e2fe94f18151 Author: Sebastian KrΓΌger Date: Mon Nov 17 10:44:49 2025 +0100 feat: initialize Convert UI - browser-based file conversion app - Add Next.js 16 with Turbopack and React 19 - Add Tailwind CSS 4 with OKLCH color system - Implement FFmpeg.wasm for video/audio conversion - Implement ImageMagick WASM for image conversion - Add file upload with drag-and-drop - Add format selector with fuzzy search - Add conversion preview and download - Add conversion history with localStorage - Add dark/light theme support - Support 22+ file formats across video, audio, and images πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c098365 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# env files +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/README.md b/README.md new file mode 100644 index 0000000..4f775dc --- /dev/null +++ b/README.md @@ -0,0 +1,224 @@ +# Convert UI + +A modern, browser-based file conversion application built with Next.js 16, Tailwind CSS 4, and WebAssembly. Convert videos, images, and documents directly in your browser without uploading files to any server. + +## Features + +- **🎬 Video Conversion** - Convert between MP4, WebM, AVI, MOV, MKV, and GIF +- **🎡 Audio Conversion** - Convert between MP3, WAV, OGG, AAC, and FLAC +- **πŸ–ΌοΈ Image Conversion** - Convert between PNG, JPG, WebP, GIF, BMP, TIFF, and SVG +- **πŸ“„ Document Conversion** - (Coming soon) Convert between PDF, DOCX, Markdown, HTML, and TXT +- **πŸ”’ Privacy First** - All conversions happen locally in your browser, no server uploads +- **⚑ Fast & Efficient** - Powered by WebAssembly for near-native performance +- **🎨 Beautiful UI** - Modern, responsive design with dark/light theme support +- **πŸ” Smart Search** - Fuzzy search for quick format selection +- **πŸ“ Conversion History** - Track your recent conversions +- **🎯 Drag & Drop** - Easy file upload with drag-and-drop support + +## Tech Stack + +- **Next.js 16** - React framework with App Router and static export +- **React 19** - Latest React with concurrent features +- **TypeScript 5** - Type-safe development +- **Tailwind CSS 4** - Utility-first CSS with OKLCH color system +- **FFmpeg.wasm** - Video and audio conversion +- **ImageMagick WASM** - Image processing and conversion +- **Fuse.js** - Fuzzy search for format selection +- **Lucide React** - Beautiful icon library + +## Getting Started + +### Prerequisites + +- Node.js 22+ (managed via nvm) +- pnpm (enabled via corepack) + +### Installation + +```bash +# Clone the repository +git clone +cd convert-ui + +# Install dependencies +pnpm install + +# Run development server +pnpm dev +``` + +Open [http://localhost:3000](http://localhost:3000) to view the app. + +### Build for Production + +```bash +# Build static export +pnpm build + +# Output will be in the /out directory +``` + +## Project Structure + +``` +convert-ui/ +β”œβ”€β”€ app/ # Next.js App Router +β”‚ β”œβ”€β”€ layout.tsx # Root layout with theme +β”‚ β”œβ”€β”€ page.tsx # Main page +β”‚ └── globals.css # Global styles +β”œβ”€β”€ components/ +β”‚ β”œβ”€β”€ converter/ # Converter components +β”‚ β”‚ β”œβ”€β”€ FileConverter.tsx # Main state manager +β”‚ β”‚ β”œβ”€β”€ FileUpload.tsx # Drag-and-drop upload +β”‚ β”‚ β”œβ”€β”€ FormatSelector.tsx # Format selection with search +β”‚ β”‚ β”œβ”€β”€ ConversionPreview.tsx # Preview and download +β”‚ β”‚ └── ConversionOptions.tsx # Format-specific options +β”‚ β”œβ”€β”€ layout/ +β”‚ β”‚ └── ThemeToggle.tsx # Dark/light theme toggle +β”‚ └── ui/ # Reusable UI components +β”‚ β”œβ”€β”€ Button.tsx +β”‚ β”œβ”€β”€ Card.tsx +β”‚ β”œβ”€β”€ Toast.tsx +β”‚ β”œβ”€β”€ Progress.tsx +β”‚ β”œβ”€β”€ Skeleton.tsx +β”‚ └── Input.tsx +β”œβ”€β”€ lib/ +β”‚ β”œβ”€β”€ converters/ # Conversion services +β”‚ β”‚ β”œβ”€β”€ ffmpegService.ts # Video/audio conversion +β”‚ β”‚ β”œβ”€β”€ imagemagickService.ts # Image conversion +β”‚ β”‚ └── pandocService.ts # Document conversion (placeholder) +β”‚ β”œβ”€β”€ wasm/ +β”‚ β”‚ └── wasmLoader.ts # WASM module lazy loading +β”‚ β”œβ”€β”€ storage/ +β”‚ β”‚ └── history.ts # Conversion history +β”‚ └── utils/ +β”‚ β”œβ”€β”€ cn.ts # Class name utility +β”‚ β”œβ”€β”€ fileUtils.ts # File operations +β”‚ β”œβ”€β”€ formatMappings.ts # Supported formats +β”‚ └── debounce.ts # Debounce utility +└── types/ + └── conversion.ts # TypeScript interfaces +``` + +## Supported Formats + +### Video (FFmpeg) +- **Input/Output:** MP4, WebM, AVI, MOV, MKV, GIF + +### Audio (FFmpeg) +- **Input/Output:** MP3, WAV, OGG, AAC, FLAC + +### Images (ImageMagick) +- **Input/Output:** PNG, JPG, WebP, GIF, BMP, TIFF, SVG + +### Documents (Coming Soon) +- **Planned:** PDF, DOCX, Markdown, HTML, Plain Text + +## How It Works + +1. **File Upload** - Users can drag-and-drop or click to select a file +2. **Format Detection** - The app automatically detects the input format +3. **Format Selection** - Choose from compatible output formats +4. **Conversion** - WASM modules are loaded on-demand and process the file +5. **Download** - Preview and download the converted file + +### WASM Architecture + +- **Lazy Loading** - WASM modules are only loaded when needed +- **Memory Management** - Proper cleanup after each conversion +- **Progress Tracking** - Real-time progress updates during conversion +- **Error Handling** - Graceful error handling with user-friendly messages + +## Browser Compatibility + +- **Chrome/Edge** - Full support +- **Firefox** - Full support +- **Safari** - Full support (with SharedArrayBuffer) + +**Note:** Some features require SharedArrayBuffer support. The app sets the required COOP/COEP headers for this. + +## Performance + +- **File Size Limit** - 500MB (configurable) +- **Conversion Speed** - Varies by file size and format + - Images: < 5s for typical files + - Videos: Depends on length and quality +- **Memory Usage** - Managed automatically with cleanup + +## Development + +### Scripts + +```bash +# Development server with Turbopack +pnpm dev + +# Build for production +pnpm build + +# Run production server +pnpm start + +# Lint code +pnpm lint +``` + +### Adding New Formats + +1. Add format to `lib/utils/formatMappings.ts` +2. Implement converter in appropriate service +3. Update type definitions in `types/conversion.ts` + +### Customizing Theme + +Colors are defined in `app/globals.css` using OKLCH color space. Modify CSS variables to change the theme: + +```css +:root { + --primary: oklch(22.4% 0.053 285.8); + --background: oklch(100% 0 0); + /* ... other colors */ +} +``` + +## Docker Deployment + +```bash +# Build Docker image +docker build -t convert-ui . + +# Run container +docker run -p 80:80 convert-ui +``` + +The Dockerfile uses a multi-stage build with Nginx to serve the static export. + +## Contributing + +Contributions are welcome! Please follow these steps: + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Test thoroughly +5. Submit a pull request + +## License + +MIT License - See LICENSE file for details + +## Acknowledgments + +- [FFmpeg.wasm](https://github.com/ffmpegwasm/ffmpeg.wasm) - Video/audio conversion +- [ImageMagick WASM](https://github.com/dlemstra/magick-wasm) - Image processing +- [Next.js](https://nextjs.org/) - React framework +- [Tailwind CSS](https://tailwindcss.com/) - Styling +- [Fuse.js](https://fusejs.io/) - Fuzzy search + +## Support + +For issues, questions, or suggestions, please open an issue on GitHub. + +--- + +**Made with ❀️ using Next.js 16 and WebAssembly** diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..e54d6f3 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,275 @@ +@import "tailwindcss"; + +/* Source directives - scan components for Tailwind classes */ +@source "../components/converter/*.{js,ts,jsx,tsx}"; +@source "../components/layout/*.{js,ts,jsx,tsx}"; +@source "../components/ui/*.{js,ts,jsx,tsx}"; +@source "*.{js,ts,jsx,tsx}"; + +/* Custom dark mode variant */ +@custom-variant dark (&:is(.dark *)); + +/* CSS Variables for theming */ +@layer base { + :root { + /* Light mode colors using OKLCH */ + --background: oklch(100% 0 0); + --foreground: oklch(9.8% 0.038 285.8); + + --card: oklch(100% 0 0); + --card-foreground: oklch(9.8% 0.038 285.8); + + --popover: oklch(100% 0 0); + --popover-foreground: oklch(9.8% 0.038 285.8); + + --primary: oklch(22.4% 0.053 285.8); + --primary-foreground: oklch(98% 0 0); + + --secondary: oklch(96.1% 0 0); + --secondary-foreground: oklch(13.8% 0.038 285.8); + + --muted: oklch(96.1% 0 0); + --muted-foreground: oklch(45.1% 0.015 285.9); + + --accent: oklch(96.1% 0 0); + --accent-foreground: oklch(13.8% 0.038 285.8); + + --destructive: oklch(60.2% 0.168 29.2); + --destructive-foreground: oklch(98% 0 0); + + --border: oklch(89.8% 0 0); + --input: oklch(89.8% 0 0); + --ring: oklch(22.4% 0.053 285.8); + + --radius: 0.5rem; + + --success: oklch(60% 0.15 145); + --success-foreground: oklch(98% 0 0); + + --warning: oklch(75% 0.15 85); + --warning-foreground: oklch(20% 0 0); + + --info: oklch(65% 0.15 240); + --info-foreground: oklch(98% 0 0); + } + + .dark { + /* Dark mode colors using OKLCH */ + --background: oklch(9.8% 0.038 285.8); + --foreground: oklch(98% 0 0); + + --card: oklch(9.8% 0.038 285.8); + --card-foreground: oklch(98% 0 0); + + --popover: oklch(9.8% 0.038 285.8); + --popover-foreground: oklch(98% 0 0); + + --primary: oklch(98% 0 0); + --primary-foreground: oklch(13.8% 0.038 285.8); + + --secondary: oklch(17.7% 0.038 285.8); + --secondary-foreground: oklch(98% 0 0); + + --muted: oklch(17.7% 0.038 285.8); + --muted-foreground: oklch(63.9% 0.012 285.9); + + --accent: oklch(17.7% 0.038 285.8); + --accent-foreground: oklch(98% 0 0); + + --destructive: oklch(50% 0.2 29.2); + --destructive-foreground: oklch(98% 0 0); + + --border: oklch(17.7% 0.038 285.8); + --input: oklch(17.7% 0.038 285.8); + --ring: oklch(83.1% 0.012 285.9); + + --success: oklch(55% 0.15 145); + --success-foreground: oklch(98% 0 0); + + --warning: oklch(70% 0.15 85); + --warning-foreground: oklch(20% 0 0); + + --info: oklch(60% 0.15 240); + --info-foreground: oklch(98% 0 0); + } +} + +/* Theme inline - map CSS variables to Tailwind colors */ +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-success: var(--success); + --color-success-foreground: var(--success-foreground); + --color-warning: var(--warning); + --color-warning-foreground: var(--warning-foreground); + --color-info: var(--info); + --color-info-foreground: var(--info-foreground); + + --radius: var(--radius); +} + +/* Global styles */ +@layer base { + * { + @apply border-border; + } + + body { + @apply bg-background text-foreground; + font-feature-settings: "rlig" 1, "calt" 1; + } +} + +/* Custom animations */ +@layer utilities { + @keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + + @keyframes slideInFromRight { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } + } + + @keyframes slideDown { + from { + transform: translateY(-10px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } + } + + @keyframes slideUp { + from { + transform: translateY(10px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } + } + + @keyframes scaleIn { + from { + transform: scale(0.95); + opacity: 0; + } + to { + transform: scale(1); + opacity: 1; + } + } + + @keyframes pulseSubtle { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.8; + } + } + + @keyframes shimmer { + 0% { + background-position: -1000px 0; + } + 100% { + background-position: 1000px 0; + } + } + + @keyframes progress { + 0% { + transform: translateX(-100%); + } + 100% { + transform: translateX(100%); + } + } + + .animate-fadeIn { + animation: fadeIn 0.2s ease-out; + } + + .animate-slideInFromRight { + animation: slideInFromRight 0.3s ease-out; + } + + .animate-slideDown { + animation: slideDown 0.3s ease-out; + } + + .animate-slideUp { + animation: slideUp 0.3s ease-out; + } + + .animate-scaleIn { + animation: scaleIn 0.2s ease-out; + } + + .animate-pulseSubtle { + animation: pulseSubtle 2s ease-in-out infinite; + } + + .animate-shimmer { + animation: shimmer 2s linear infinite; + } + + .animate-progress { + animation: progress 1.5s ease-in-out infinite; + } +} + +/* Custom scrollbar */ +@layer utilities { + .custom-scrollbar::-webkit-scrollbar { + width: 8px; + height: 8px; + } + + .custom-scrollbar::-webkit-scrollbar-track { + @apply bg-muted; + border-radius: 4px; + } + + .custom-scrollbar::-webkit-scrollbar-thumb { + @apply bg-muted-foreground/30; + border-radius: 4px; + } + + .custom-scrollbar::-webkit-scrollbar-thumb:hover { + @apply bg-muted-foreground/50; + } +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..0cf31b8 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,40 @@ +import type { Metadata } from 'next'; +import './globals.css'; + +export const metadata: Metadata = { + title: 'Convert UI - File Conversion in Your Browser', + description: 'Convert videos, images, and documents directly in your browser using WebAssembly. No uploads, complete privacy.', + keywords: ['file conversion', 'video converter', 'image converter', 'document converter', 'ffmpeg', 'imagemagick', 'pandoc', 'wasm'], +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + +