refactor: streamline, refine and polish
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import * as React from 'react';
|
||||
import { Slider } from '@/components/ui/slider';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -37,7 +38,7 @@ export function ConversionOptionsPanel({
|
||||
<div className="space-y-4">
|
||||
{/* Video Codec */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground block">Video Codec</label>
|
||||
<Label>Video Codec</Label>
|
||||
<Select
|
||||
value={options.videoCodec || 'default'}
|
||||
onValueChange={(value) => handleOptionChange('videoCodec', value === 'default' ? undefined : value)}
|
||||
@@ -59,7 +60,7 @@ export function ConversionOptionsPanel({
|
||||
{/* Video Bitrate */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium text-foreground">Video Bitrate</label>
|
||||
<Label>Video Bitrate</Label>
|
||||
<span className="text-xs text-muted-foreground">{options.videoBitrate || '2M'}</span>
|
||||
</div>
|
||||
<Slider
|
||||
@@ -75,7 +76,7 @@ export function ConversionOptionsPanel({
|
||||
|
||||
{/* Resolution */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground block">Resolution</label>
|
||||
<Label>Resolution</Label>
|
||||
<Select
|
||||
value={options.videoResolution || 'original'}
|
||||
onValueChange={(value) => handleOptionChange('videoResolution', value === 'original' ? undefined : value)}
|
||||
@@ -96,7 +97,7 @@ export function ConversionOptionsPanel({
|
||||
|
||||
{/* FPS */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground block">Frame Rate (FPS)</label>
|
||||
<Label>Frame Rate (FPS)</Label>
|
||||
<Select
|
||||
value={options.videoFps?.toString() || 'original'}
|
||||
onValueChange={(value) => handleOptionChange('videoFps', value === 'original' ? undefined : parseInt(value))}
|
||||
@@ -118,7 +119,7 @@ export function ConversionOptionsPanel({
|
||||
{/* Audio Bitrate */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium text-foreground">Audio Bitrate</label>
|
||||
<Label>Audio Bitrate</Label>
|
||||
<span className="text-xs text-muted-foreground">{options.audioBitrate || '128k'}</span>
|
||||
</div>
|
||||
<Slider
|
||||
@@ -137,7 +138,7 @@ export function ConversionOptionsPanel({
|
||||
<div className="space-y-4">
|
||||
{/* Audio Codec */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground block">Audio Codec</label>
|
||||
<Label>Audio Codec</Label>
|
||||
<Select
|
||||
value={options.audioCodec || 'default'}
|
||||
onValueChange={(value) => handleOptionChange('audioCodec', value === 'default' ? undefined : value)}
|
||||
@@ -160,7 +161,7 @@ export function ConversionOptionsPanel({
|
||||
{/* Bitrate */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium text-foreground">Bitrate</label>
|
||||
<Label>Bitrate</Label>
|
||||
<span className="text-xs text-muted-foreground">{options.audioBitrate || '192k'}</span>
|
||||
</div>
|
||||
<Slider
|
||||
@@ -175,7 +176,7 @@ export function ConversionOptionsPanel({
|
||||
|
||||
{/* Sample Rate */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground block">Sample Rate</label>
|
||||
<Label>Sample Rate</Label>
|
||||
<Select
|
||||
value={options.audioSampleRate?.toString() || 'original'}
|
||||
onValueChange={(value) => handleOptionChange('audioSampleRate', value === 'original' ? undefined : parseInt(value))}
|
||||
@@ -195,7 +196,7 @@ export function ConversionOptionsPanel({
|
||||
|
||||
{/* Channels */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground block">Channels</label>
|
||||
<Label>Channels</Label>
|
||||
<Select
|
||||
value={options.audioChannels?.toString() || 'original'}
|
||||
onValueChange={(value) => handleOptionChange('audioChannels', value === 'original' ? undefined : parseInt(value))}
|
||||
@@ -219,7 +220,7 @@ export function ConversionOptionsPanel({
|
||||
{/* Quality */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium text-foreground">Quality</label>
|
||||
<Label>Quality</Label>
|
||||
<span className="text-xs text-muted-foreground">{options.imageQuality || 85}%</span>
|
||||
</div>
|
||||
<Slider
|
||||
@@ -234,7 +235,7 @@ export function ConversionOptionsPanel({
|
||||
|
||||
{/* Width */}
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground mb-2 block">Width (px)</label>
|
||||
<Label className="mb-2">Width (px)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={options.imageWidth || ''}
|
||||
@@ -247,7 +248,7 @@ export function ConversionOptionsPanel({
|
||||
|
||||
{/* Height */}
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground mb-2 block">Height (px)</label>
|
||||
<Label className="mb-2">Height (px)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={options.imageHeight || ''}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { Download, CheckCircle, XCircle, Loader2, Clock, TrendingUp, FileCheck2, ArrowRight, RefreshCw } from 'lucide-react';
|
||||
import { Download, CheckCircle, XCircle, Loader2, Clock, TrendingUp, RefreshCw } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
@@ -85,7 +85,7 @@ export function ConversionPreview({ job, onDownload, onRetry }: ConversionPrevie
|
||||
switch (category) {
|
||||
case 'image':
|
||||
return (
|
||||
<div className="mt-4 rounded-lg overflow-hidden bg-muted/30 flex items-center justify-center p-4">
|
||||
<div className="mt-3 rounded-lg overflow-hidden bg-muted/30 flex items-center justify-center p-4">
|
||||
<img
|
||||
src={previewUrl}
|
||||
alt="Converted image preview"
|
||||
@@ -107,7 +107,7 @@ export function ConversionPreview({ job, onDownload, onRetry }: ConversionPrevie
|
||||
|
||||
case 'video':
|
||||
return (
|
||||
<div className="mt-4 rounded-lg overflow-hidden bg-muted/30">
|
||||
<div className="mt-3 rounded-lg overflow-hidden bg-muted/30">
|
||||
<video src={previewUrl} controls className="w-full max-h-64">
|
||||
Your browser does not support video playback.
|
||||
</video>
|
||||
@@ -140,38 +140,38 @@ export function ConversionPreview({ job, onDownload, onRetry }: ConversionPrevie
|
||||
switch (job.status) {
|
||||
case 'loading':
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2 text-info">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
<span className="text-sm font-medium">Loading WASM converter...</span>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin text-primary" />
|
||||
<span className="text-xs font-medium">Loading converter...</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
<span>Elapsed: {formatTime(elapsedTime)}</span>
|
||||
<div className="flex items-center gap-1.5 text-[10px] text-muted-foreground">
|
||||
<Clock className="h-3 w-3" />
|
||||
<span>{formatTime(elapsedTime)}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'processing':
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-info">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
<span className="text-sm font-medium">Converting...</span>
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin text-primary" />
|
||||
<span className="text-xs font-medium">Converting...</span>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">{job.progress}%</span>
|
||||
<span className="text-[10px] text-muted-foreground tabular-nums">{job.progress}%</span>
|
||||
</div>
|
||||
<Progress value={job.progress} />
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4 text-xs text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
<span>Elapsed: {formatTime(elapsedTime)}</span>
|
||||
<Progress value={job.progress} className="h-1" />
|
||||
<div className="flex items-center gap-3 text-[10px] text-muted-foreground">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Clock className="h-3 w-3" />
|
||||
<span>{formatTime(elapsedTime)}</span>
|
||||
</div>
|
||||
{estimatedTimeRemaining && (
|
||||
<div className="flex items-center gap-2">
|
||||
<TrendingUp className="h-3.5 w-3.5" />
|
||||
<span>~{formatTime(estimatedTimeRemaining)} remaining</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<TrendingUp className="h-3 w-3" />
|
||||
<span>~{formatTime(estimatedTimeRemaining)} left</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -184,39 +184,27 @@ export function ConversionPreview({ job, onDownload, onRetry }: ConversionPrevie
|
||||
const sizeReduction = inputSize > 0 ? ((inputSize - outputSize) / inputSize) * 100 : 0;
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2 text-success">
|
||||
<CheckCircle className="h-5 w-5" />
|
||||
<span className="text-sm font-medium">Conversion complete!</span>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="h-3.5 w-3.5 text-primary" />
|
||||
<span className="text-xs font-medium">Complete</span>
|
||||
</div>
|
||||
|
||||
{/* File size comparison */}
|
||||
<div className="bg-muted/50 rounded-lg p-3 space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileCheck2 className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">Input:</span>
|
||||
</div>
|
||||
<span className="font-medium">{formatFileSize(inputSize)}</span>
|
||||
<div className="bg-muted/50 rounded-lg p-2.5 space-y-1">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-muted-foreground">Input</span>
|
||||
<span className="font-medium tabular-nums">{formatFileSize(inputSize)}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center py-1">
|
||||
<ArrowRight className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileCheck2 className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">Output:</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{formatFileSize(outputSize)}</span>
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-muted-foreground">Output</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="font-medium tabular-nums">{formatFileSize(outputSize)}</span>
|
||||
{Math.abs(sizeReduction) > 1 && (
|
||||
<span className={cn(
|
||||
"text-xs px-2 py-0.5 rounded-full",
|
||||
"text-[10px] px-1.5 py-0.5 rounded-full",
|
||||
sizeReduction > 0
|
||||
? "bg-success/10 text-success"
|
||||
: "bg-info/10 text-info"
|
||||
? "bg-primary/10 text-primary"
|
||||
: "bg-muted text-muted-foreground"
|
||||
)}>
|
||||
{sizeReduction > 0 ? '-' : '+'}{Math.abs(sizeReduction).toFixed(0)}%
|
||||
</span>
|
||||
@@ -230,8 +218,8 @@ export function ConversionPreview({ job, onDownload, onRetry }: ConversionPrevie
|
||||
case 'error':
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-destructive">
|
||||
<XCircle className="h-5 w-5" />
|
||||
<span className="text-sm font-medium">Conversion failed</span>
|
||||
<XCircle className="h-3.5 w-3.5" />
|
||||
<span className="text-xs font-medium">Conversion failed</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -245,48 +233,41 @@ export function ConversionPreview({ job, onDownload, onRetry }: ConversionPrevie
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="animate-fadeIn">
|
||||
<Card className="animate-fade-in">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Conversion Status</CardTitle>
|
||||
<CardTitle className="text-sm">Conversion</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{/* Status */}
|
||||
<div className="space-y-3">
|
||||
{renderStatus()}
|
||||
|
||||
{/* Error message */}
|
||||
{job.error && (
|
||||
<div className="bg-destructive/10 border border-destructive/20 rounded-md p-3">
|
||||
<p className="text-sm text-destructive">{job.error}</p>
|
||||
<div className="bg-destructive/10 border border-destructive/20 rounded-md p-2.5">
|
||||
<p className="text-xs text-destructive">{job.error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Retry button */}
|
||||
{job.status === 'error' && onRetry && (
|
||||
<Button onClick={onRetry} variant="outline" className="w-full gap-2">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
Retry Conversion
|
||||
<Button onClick={onRetry} variant="outline" className="w-full">
|
||||
<RefreshCw className="h-3.5 w-3.5 mr-1.5" />
|
||||
Retry
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Preview */}
|
||||
{job.status === 'completed' && renderPreview()}
|
||||
|
||||
{/* Download button */}
|
||||
{job.status === 'completed' && job.result && (
|
||||
<Button onClick={handleDownload} className="w-full" variant="default" size="lg">
|
||||
<Download className="h-4 w-4 shrink-0" />
|
||||
<Button onClick={handleDownload} className="w-full">
|
||||
<Download className="h-3.5 w-3.5 shrink-0 mr-1.5" />
|
||||
<span className="truncate min-w-0">
|
||||
Download{' '}
|
||||
{generateOutputFilename(job.inputFile.name, job.outputFormat.extension)}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Duration */}
|
||||
{job.status === 'completed' && job.startTime && job.endTime && (
|
||||
<p className="text-xs text-muted-foreground text-center">
|
||||
Completed in {((job.endTime - job.startTime) / 1000).toFixed(2)}s
|
||||
<p className="text-[10px] text-muted-foreground text-center">
|
||||
{((job.endTime - job.startTime) / 1000).toFixed(1)}s
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import * as React from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Slider } from '@/components/ui/slider';
|
||||
import {
|
||||
@@ -366,13 +367,12 @@ export function FileConverter() {
|
||||
const completedCount = conversionJobs.filter(job => job.status === 'completed').length;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 w-full">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 w-full">
|
||||
{/* Left Column: Upload and Conversion Options */}
|
||||
<div className="space-y-6">
|
||||
{/* Upload Card */}
|
||||
<Card className="glass">
|
||||
<CardContent className="p-6">
|
||||
{/* File upload */}
|
||||
<Card>
|
||||
<CardContent>
|
||||
<FileUpload
|
||||
onFileSelect={handleFileSelect}
|
||||
onFileRemove={handleFileRemove}
|
||||
@@ -386,14 +386,14 @@ export function FileConverter() {
|
||||
|
||||
{/* Conversion Options Card */}
|
||||
{inputFormat && compatibleFormats.length > 0 && (
|
||||
<Card className="glass">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Conversion Options</CardTitle>
|
||||
<CardTitle>Options</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<CardContent className="space-y-4">
|
||||
{/* Output Format Select */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground block">Output Format</label>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs">Output Format</Label>
|
||||
<Select
|
||||
value={outputFormat?.id || ''}
|
||||
onValueChange={(formatId) => {
|
||||
@@ -422,7 +422,7 @@ export function FileConverter() {
|
||||
<div className="space-y-4 pt-4 border-t border-border">
|
||||
{/* Video Codec */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground block">Video Codec</label>
|
||||
<Label className="text-xs">Video Codec</Label>
|
||||
<Select
|
||||
value={conversionOptions.videoCodec || 'default'}
|
||||
onValueChange={(value) => setConversionOptions({ ...conversionOptions, videoCodec: value === 'default' ? undefined : value })}
|
||||
@@ -444,7 +444,7 @@ export function FileConverter() {
|
||||
{/* Video Bitrate */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium text-foreground">Video Bitrate</label>
|
||||
<Label className="text-xs">Video Bitrate</Label>
|
||||
<span className="text-xs text-muted-foreground">{conversionOptions.videoBitrate || '2M'}</span>
|
||||
</div>
|
||||
<Slider
|
||||
@@ -460,7 +460,7 @@ export function FileConverter() {
|
||||
|
||||
{/* Resolution */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground block">Resolution</label>
|
||||
<Label className="text-xs">Resolution</Label>
|
||||
<Select
|
||||
value={conversionOptions.videoResolution || 'original'}
|
||||
onValueChange={(value) => setConversionOptions({ ...conversionOptions, videoResolution: value === 'original' ? undefined : value })}
|
||||
@@ -481,7 +481,7 @@ export function FileConverter() {
|
||||
|
||||
{/* FPS */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground block">Frame Rate (FPS)</label>
|
||||
<Label className="text-xs">Frame Rate (FPS)</Label>
|
||||
<Select
|
||||
value={conversionOptions.videoFps?.toString() || 'original'}
|
||||
onValueChange={(value) => setConversionOptions({ ...conversionOptions, videoFps: value === 'original' ? undefined : parseInt(value) })}
|
||||
@@ -503,7 +503,7 @@ export function FileConverter() {
|
||||
{/* Audio Bitrate */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium text-foreground">Audio Bitrate</label>
|
||||
<Label className="text-xs">Audio Bitrate</Label>
|
||||
<span className="text-xs text-muted-foreground">{conversionOptions.audioBitrate || '128k'}</span>
|
||||
</div>
|
||||
<Slider
|
||||
@@ -522,7 +522,7 @@ export function FileConverter() {
|
||||
<div className="space-y-4 pt-4 border-t border-border">
|
||||
{/* Audio Codec */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground block">Audio Codec</label>
|
||||
<Label className="text-xs">Audio Codec</Label>
|
||||
<Select
|
||||
value={conversionOptions.audioCodec || 'default'}
|
||||
onValueChange={(value) => setConversionOptions({ ...conversionOptions, audioCodec: value === 'default' ? undefined : value })}
|
||||
@@ -545,7 +545,7 @@ export function FileConverter() {
|
||||
{/* Bitrate */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium text-foreground">Bitrate</label>
|
||||
<Label className="text-xs">Bitrate</Label>
|
||||
<span className="text-xs text-muted-foreground">{conversionOptions.audioBitrate || '192k'}</span>
|
||||
</div>
|
||||
<Slider
|
||||
@@ -560,7 +560,7 @@ export function FileConverter() {
|
||||
|
||||
{/* Sample Rate */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground block">Sample Rate</label>
|
||||
<Label className="text-xs">Sample Rate</Label>
|
||||
<Select
|
||||
value={conversionOptions.audioSampleRate?.toString() || 'original'}
|
||||
onValueChange={(value) => setConversionOptions({ ...conversionOptions, audioSampleRate: value === 'original' ? undefined : parseInt(value) })}
|
||||
@@ -580,7 +580,7 @@ export function FileConverter() {
|
||||
|
||||
{/* Channels */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground block">Channels</label>
|
||||
<Label className="text-xs">Channels</Label>
|
||||
<Select
|
||||
value={conversionOptions.audioChannels?.toString() || 'original'}
|
||||
onValueChange={(value) => setConversionOptions({ ...conversionOptions, audioChannels: value === 'original' ? undefined : parseInt(value) })}
|
||||
@@ -604,7 +604,7 @@ export function FileConverter() {
|
||||
{/* Quality */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium text-foreground">Quality</label>
|
||||
<Label className="text-xs">Quality</Label>
|
||||
<span className="text-xs text-muted-foreground">{conversionOptions.imageQuality || 85}%</span>
|
||||
</div>
|
||||
<Slider
|
||||
@@ -619,7 +619,7 @@ export function FileConverter() {
|
||||
|
||||
{/* Width */}
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground mb-2 block">Width (px)</label>
|
||||
<Label className="text-xs mb-1.5">Width (px)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={conversionOptions.imageWidth || ''}
|
||||
@@ -632,7 +632,7 @@ export function FileConverter() {
|
||||
|
||||
{/* Height */}
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground mb-2 block">Height (px)</label>
|
||||
<Label className="text-xs mb-1.5">Height (px)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={conversionOptions.imageHeight || ''}
|
||||
@@ -652,13 +652,12 @@ export function FileConverter() {
|
||||
onClick={handleConvert}
|
||||
disabled={isConvertDisabled}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
>
|
||||
{isConverting
|
||||
? 'Converting...'
|
||||
: `Convert ${selectedFiles.length} File${selectedFiles.length > 1 ? 's' : ''}`}
|
||||
</Button>
|
||||
<Button onClick={handleReset} variant="outline" size="lg" className="w-full">
|
||||
<Button onClick={handleReset} variant="outline" className="w-full">
|
||||
Reset
|
||||
</Button>
|
||||
</CardContent>
|
||||
@@ -670,13 +669,11 @@ export function FileConverter() {
|
||||
<div className="space-y-6">
|
||||
{/* Download All Button */}
|
||||
{completedCount > 0 && (
|
||||
<Card className="glass">
|
||||
<CardContent className="p-6">
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Button
|
||||
onClick={handleDownloadAll}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
variant="default"
|
||||
>
|
||||
Download All ({completedCount} file{completedCount > 1 ? 's' : ''})
|
||||
{completedCount > 1 && ' as ZIP'}
|
||||
|
||||
@@ -246,13 +246,13 @@ export function FileUpload({
|
||||
const metadata = fileMetadata[index];
|
||||
return (
|
||||
<div key={`${file.name}-${index}`} className="border border-border rounded-xl p-4 bg-card/50 backdrop-blur-sm">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="p-2 bg-primary/10 rounded-lg shrink-0">
|
||||
{getCategoryIcon()}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<p className="text-sm font-semibold text-foreground truncate" title={file.name}>
|
||||
<p className="text-sm font-medium text-foreground truncate" title={file.name}>
|
||||
{file.name}
|
||||
</p>
|
||||
<Button
|
||||
@@ -267,22 +267,22 @@ export function FileUpload({
|
||||
</Button>
|
||||
</div>
|
||||
{metadata && (
|
||||
<div className="mt-2 flex flex-wrap gap-4 text-[10px] text-muted-foreground uppercase tracking-wider font-bold">
|
||||
<div className="mt-1.5 flex flex-wrap gap-3 text-[10px] text-muted-foreground">
|
||||
{/* File Size */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="flex items-center gap-1">
|
||||
<HardDrive className="h-3 w-3" />
|
||||
<span>{metadata.size}</span>
|
||||
</div>
|
||||
|
||||
{/* Type */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="flex items-center gap-1">
|
||||
<File className="h-3 w-3" />
|
||||
<span>{metadata.type}</span>
|
||||
</div>
|
||||
|
||||
{/* Duration (for video/audio) */}
|
||||
{metadata.duration && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock className="h-3 w-3" />
|
||||
<span>{metadata.duration}</span>
|
||||
</div>
|
||||
@@ -290,7 +290,7 @@ export function FileUpload({
|
||||
|
||||
{/* Dimensions */}
|
||||
{metadata.dimensions && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="flex items-center gap-1">
|
||||
{inputFormat?.category === 'video' ? (
|
||||
<Film className="h-3 w-3" />
|
||||
) : (
|
||||
@@ -312,10 +312,9 @@ export function FileUpload({
|
||||
variant="outline"
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
className="w-full rounded-xl"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
>
|
||||
<Upload className="h-4 w-4 mr-2" />
|
||||
<Upload className="h-3.5 w-3.5 mr-1.5" />
|
||||
Add More Files
|
||||
</Button>
|
||||
</div>
|
||||
@@ -327,23 +326,23 @@ export function FileUpload({
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
className={cn(
|
||||
'border-2 border-dashed rounded-xl p-10 text-center cursor-pointer transition-all duration-300',
|
||||
'hover:border-primary/50 hover:bg-primary/5',
|
||||
'border-2 border-dashed rounded-xl p-8 text-center cursor-pointer transition-all duration-200',
|
||||
'hover:border-primary/40 hover:bg-primary/5',
|
||||
{
|
||||
'border-primary bg-primary/10 scale-[0.98]': isDragging,
|
||||
'border-border bg-muted/30': !isDragging,
|
||||
'border-border/50': !isDragging,
|
||||
'opacity-50 cursor-not-allowed': disabled,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="bg-primary/10 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Upload className="h-8 w-8 text-primary" />
|
||||
<div className="bg-primary/10 w-12 h-12 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||
<Upload className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<p className="text-sm font-semibold text-foreground mb-1">
|
||||
Drop your files here or click to browse
|
||||
<p className="text-sm font-medium text-foreground mb-0.5">
|
||||
Drop files here or click to browse
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Maximum file size: {maxSizeMB}MB per file
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
Max {maxSizeMB}MB per file
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -5,6 +5,7 @@ import Fuse from 'fuse.js';
|
||||
import { Search } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import type { ConversionFormat } from '@/types/media';
|
||||
|
||||
@@ -62,7 +63,7 @@ export function FormatSelector({
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<label className="text-sm font-medium text-foreground mb-2 block">{label}</label>
|
||||
<Label className="mb-2">{label}</Label>
|
||||
|
||||
{/* Search input */}
|
||||
<div className="relative mb-3">
|
||||
|
||||
Reference in New Issue
Block a user