From b560dcbc8e9e67e3db09314695948ec34d57b8c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Wed, 25 Feb 2026 19:59:22 +0100 Subject: [PATCH] refactor: streamline media converter UI and layout - consolidated file upload and metadata display in single card - replaced complex FormatSelector with simple shadcn Select component - inlined all conversion options without toggle display - restructured layout to 2-column grid matching pastel app pattern: - left column: upload and conversion options - right column: conversion results - removed unused components (FileInfo, FormatSelector, ConversionOptionsPanel) - cleaned up imports and simplified component hierarchy Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitignore | 4 + components/media/ConversionPreview.tsx | 2 +- components/media/FileConverter.tsx | 427 +++++++++++++++++++------ components/media/FileInfo.tsx | 210 ------------ components/media/FileUpload.tsx | 215 +++++++++++-- 5 files changed, 524 insertions(+), 334 deletions(-) delete mode 100644 components/media/FileInfo.tsx diff --git a/.gitignore b/.gitignore index 2840e4e..8151bc1 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,7 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# agents +.github +.claude \ No newline at end of file diff --git a/components/media/ConversionPreview.tsx b/components/media/ConversionPreview.tsx index 89588f8..063aece 100644 --- a/components/media/ConversionPreview.tsx +++ b/components/media/ConversionPreview.tsx @@ -274,7 +274,7 @@ export function ConversionPreview({ job, onDownload, onRetry }: ConversionPrevie {/* Download button */} {job.status === 'completed' && job.result && ( - - - - )} - - + + + )} + - {/* Download All Button */} - {completedCount > 0 && ( - - - - - - )} + {/* Right Column: Results */} +
+ {/* Download All Button */} + {completedCount > 0 && ( + + + + + + )} - {/* Conversion previews */} - {conversionJobs.length > 0 && ( -
- {conversionJobs.map((job) => ( - handleRetry(job.id)} - /> - ))} -
- )} + {/* Conversion previews */} + {conversionJobs.length > 0 && ( +
+ {conversionJobs.map((job) => ( + handleRetry(job.id)} + /> + ))} +
+ )} +
); } diff --git a/components/media/FileInfo.tsx b/components/media/FileInfo.tsx deleted file mode 100644 index 5c3bf1d..0000000 --- a/components/media/FileInfo.tsx +++ /dev/null @@ -1,210 +0,0 @@ -'use client'; - -import * as React from 'react'; -import { File, FileVideo, FileAudio, FileImage, Clock, HardDrive, Film, Music } from 'lucide-react'; -import { Card } from '@/components/ui/card'; -import type { ConversionFormat } from '@/types/media'; - -interface FileInfoProps { - file: File; - format: ConversionFormat; -} - -interface FileMetadata { - name: string; - size: string; - type: string; - category: string; - duration?: string; - dimensions?: string; -} - -export function FileInfo({ file, format }: FileInfoProps) { - const [metadata, setMetadata] = React.useState(null); - - React.useEffect(() => { - extractMetadata(file, format); - }, [file, format]); - - const extractMetadata = async (file: File, format: ConversionFormat) => { - const sizeInMB = (file.size / (1024 * 1024)).toFixed(2); - const baseMetadata: FileMetadata = { - name: file.name, - size: file.size < 1024 * 1024 ? `${(file.size / 1024).toFixed(2)} KB` : `${sizeInMB} MB`, - type: format.name, - category: format.category, - }; - - // Try to extract media-specific metadata - if (format.category === 'video' && file.type.startsWith('video/')) { - try { - const video = document.createElement('video'); - video.preload = 'metadata'; - - const promise = new Promise((resolve) => { - video.onloadedmetadata = () => { - const duration = video.duration; - const minutes = Math.floor(duration / 60); - const seconds = Math.floor(duration % 60); - const durationStr = `${minutes}:${seconds.toString().padStart(2, '0')}`; - - resolve({ - ...baseMetadata, - duration: durationStr, - dimensions: `${video.videoWidth} × ${video.videoHeight}`, - }); - - URL.revokeObjectURL(video.src); - }; - - video.onerror = () => { - resolve(baseMetadata); - URL.revokeObjectURL(video.src); - }; - }); - - video.src = URL.createObjectURL(file); - const result = await promise; - setMetadata(result); - return; - } catch (error) { - console.error('Failed to extract video metadata:', error); - } - } else if (format.category === 'audio' && file.type.startsWith('audio/')) { - try { - const audio = document.createElement('audio'); - audio.preload = 'metadata'; - - const promise = new Promise((resolve) => { - audio.onloadedmetadata = () => { - const duration = audio.duration; - const minutes = Math.floor(duration / 60); - const seconds = Math.floor(duration % 60); - const durationStr = `${minutes}:${seconds.toString().padStart(2, '0')}`; - - resolve({ - ...baseMetadata, - duration: durationStr, - }); - - URL.revokeObjectURL(audio.src); - }; - - audio.onerror = () => { - resolve(baseMetadata); - URL.revokeObjectURL(audio.src); - }; - }); - - audio.src = URL.createObjectURL(file); - const result = await promise; - setMetadata(result); - return; - } catch (error) { - console.error('Failed to extract audio metadata:', error); - } - } else if (format.category === 'image' && file.type.startsWith('image/')) { - try { - const img = new Image(); - - const promise = new Promise((resolve) => { - img.onload = () => { - resolve({ - ...baseMetadata, - dimensions: `${img.width} × ${img.height}`, - }); - - URL.revokeObjectURL(img.src); - }; - - img.onerror = () => { - resolve(baseMetadata); - URL.revokeObjectURL(img.src); - }; - }); - - img.src = URL.createObjectURL(file); - const result = await promise; - setMetadata(result); - return; - } catch (error) { - console.error('Failed to extract image metadata:', error); - } - } - - setMetadata(baseMetadata); - }; - - const getCategoryIcon = () => { - switch (format.category) { - case 'video': - return ; - case 'audio': - return ; - case 'image': - return ; - default: - return ; - } - }; - - if (!metadata) { - return ( - -
-
-
-
-
-
-
-
- ); - } - - return ( - -
-
{getCategoryIcon()}
-
-

- {metadata.name} -

-
- {/* File Size */} -
- - {metadata.size} -
- - {/* Type */} -
- - {metadata.type} -
- - {/* Duration (for video/audio) */} - {metadata.duration && ( -
- - {metadata.duration} -
- )} - - {/* Dimensions */} - {metadata.dimensions && ( -
- {format.category === 'video' ? ( - - ) : ( - - )} - {metadata.dimensions} -
- )} -
-
-
-
- ); -} diff --git a/components/media/FileUpload.tsx b/components/media/FileUpload.tsx index fb54d99..bc9c301 100644 --- a/components/media/FileUpload.tsx +++ b/components/media/FileUpload.tsx @@ -1,10 +1,10 @@ 'use client'; import * as React from 'react'; -import { Upload, X } from 'lucide-react'; +import { Upload, X, File, FileVideo, FileAudio, FileImage, Clock, HardDrive, Film } from 'lucide-react'; import { cn } from '@/lib/utils/cn'; -import { formatFileSize } from '@/lib/media/utils/fileUtils'; import { Button } from '@/components/ui/button'; +import type { ConversionFormat } from '@/types/media'; export interface FileUploadProps { onFileSelect: (files: File[]) => void; @@ -14,6 +14,7 @@ export interface FileUploadProps { maxSizeMB?: number; disabled?: boolean; inputRef?: React.RefObject; + inputFormat?: ConversionFormat; } export function FileUpload({ @@ -24,9 +25,137 @@ export function FileUpload({ maxSizeMB = 500, disabled = false, inputRef, + inputFormat, }: FileUploadProps) { const [isDragging, setIsDragging] = React.useState(false); - const fileInputRef = inputRef || React.useRef(null); + const [fileMetadata, setFileMetadata] = React.useState>({}); + const localFileInputRef = React.useRef(null); + const fileInputRef = inputRef || localFileInputRef; + + // Extract metadata for files + React.useEffect(() => { + const extractMetadata = async () => { + if (selectedFiles.length === 0 || !inputFormat) { + setFileMetadata({}); + return; + } + + const metadata: Record = {}; + + for (let i = 0; i < selectedFiles.length; i++) { + const file = selectedFiles[i]; + const baseMetadata = { + name: file.name, + size: file.size < 1024 * 1024 ? `${(file.size / 1024).toFixed(2)} KB` : `${(file.size / (1024 * 1024)).toFixed(2)} MB`, + type: inputFormat.name, + }; + + // Extract media-specific metadata + if (inputFormat.category === 'video' && file.type.startsWith('video/')) { + try { + const video = document.createElement('video'); + video.preload = 'metadata'; + + const metadataPromise = new Promise((resolve) => { + video.onloadedmetadata = () => { + const duration = video.duration; + const minutes = Math.floor(duration / 60); + const seconds = Math.floor(duration % 60); + resolve({ + ...baseMetadata, + duration: `${minutes}:${seconds.toString().padStart(2, '0')}`, + dimensions: `${video.videoWidth} × ${video.videoHeight}`, + }); + URL.revokeObjectURL(video.src); + }; + + video.onerror = () => { + resolve(baseMetadata); + URL.revokeObjectURL(video.src); + }; + }); + + video.src = URL.createObjectURL(file); + metadata[i] = await metadataPromise; + } catch (error) { + metadata[i] = baseMetadata; + } + } else if (inputFormat.category === 'audio' && file.type.startsWith('audio/')) { + try { + const audio = document.createElement('audio'); + audio.preload = 'metadata'; + + const metadataPromise = new Promise((resolve) => { + audio.onloadedmetadata = () => { + const duration = audio.duration; + const minutes = Math.floor(duration / 60); + const seconds = Math.floor(duration % 60); + resolve({ + ...baseMetadata, + duration: `${minutes}:${seconds.toString().padStart(2, '0')}`, + }); + URL.revokeObjectURL(audio.src); + }; + + audio.onerror = () => { + resolve(baseMetadata); + URL.revokeObjectURL(audio.src); + }; + }); + + audio.src = URL.createObjectURL(file); + metadata[i] = await metadataPromise; + } catch (error) { + metadata[i] = baseMetadata; + } + } else if (inputFormat.category === 'image' && file.type.startsWith('image/')) { + try { + const img = new Image(); + + const metadataPromise = new Promise((resolve) => { + img.onload = () => { + resolve({ + ...baseMetadata, + dimensions: `${img.width} × ${img.height}`, + }); + URL.revokeObjectURL(img.src); + }; + + img.onerror = () => { + resolve(baseMetadata); + URL.revokeObjectURL(img.src); + }; + }); + + img.src = URL.createObjectURL(file); + metadata[i] = await metadataPromise; + } catch (error) { + metadata[i] = baseMetadata; + } + } else { + metadata[i] = baseMetadata; + } + } + + setFileMetadata(metadata); + }; + + extractMetadata(); + }, [selectedFiles, inputFormat]); + + const getCategoryIcon = () => { + if (!inputFormat) return ; + switch (inputFormat.category) { + case 'video': + return ; + case 'audio': + return ; + case 'image': + return ; + default: + return ; + } + }; const handleDragEnter = (e: React.DragEvent) => { e.preventDefault(); @@ -113,28 +242,68 @@ export function FileUpload({ {selectedFiles.length > 0 ? (
- {selectedFiles.map((file, index) => ( -
-
-
-

{file.name}

-

- {formatFileSize(file.size)} -

+ {selectedFiles.map((file, index) => { + const metadata = fileMetadata[index]; + return ( +
+
+
{getCategoryIcon()}
+
+
+

+ {file.name} +

+ +
+ {metadata && ( +
+ {/* File Size */} +
+ + {metadata.size} +
+ + {/* Type */} +
+ + {metadata.type} +
+ + {/* Duration (for video/audio) */} + {metadata.duration && ( +
+ + {metadata.duration} +
+ )} + + {/* Dimensions */} + {metadata.dimensions && ( +
+ {inputFormat?.category === 'video' ? ( + + ) : ( + + )} + {metadata.dimensions} +
+ )} +
+ )} +
-
-
- ))} + ); + })} {/* Add more files button */}