| import { useState } from 'react' |
| import { X, Download, CheckCircle2 } from 'lucide-react' |
| import clsx from 'clsx' |
| import { downloadClip } from '../utils/api' |
|
|
| |
| |
| |
| |
| function ExportModal({ isOpen, onClose, clip, jobId }) { |
| const [format, setFormat] = useState('mp4') |
| const [quality, setQuality] = useState('1080p') |
| const [captions, setCaptions] = useState('burned') |
| const [watermark, setWatermark] = useState(true) |
| const [isExporting, setIsExporting] = useState(false) |
| const [exportComplete, setExportComplete] = useState(false) |
|
|
| if (!isOpen) return null |
|
|
| const formats = [ |
| { id: 'mp4', label: 'MP4', codec: 'H.264' }, |
| { id: 'mov', label: 'MOV', codec: 'ProRes' }, |
| { id: 'webm', label: 'WebM', codec: 'VP9' }, |
| ] |
|
|
| const qualities = [ |
| { id: '4k', label: '4K (2160p)', size: '~450MB' }, |
| { id: '1080p', label: '1080p (Full HD)', size: '~150MB' }, |
| { id: '720p', label: '720p (HD)', size: '~65MB' }, |
| { id: '480p', label: '480p (Mobile)', size: '~25MB' }, |
| ] |
|
|
| const handleExport = async () => { |
| setIsExporting(true) |
| try { |
| const blob = await downloadClip(jobId, clip.id, { |
| format, |
| quality, |
| includeCaptions: captions !== 'none', |
| includeWatermark: watermark, |
| }) |
|
|
| |
| const url = window.URL.createObjectURL(blob) |
| const link = document.createElement('a') |
| link.href = url |
| link.download = `${clip.title || 'clip'}.${format}` |
| document.body.appendChild(link) |
| link.click() |
| document.body.removeChild(link) |
| window.URL.revokeObjectURL(url) |
|
|
| setExportComplete(true) |
| setTimeout(() => { |
| onClose() |
| setExportComplete(false) |
| }, 2000) |
| } catch (error) { |
| console.error('Export failed:', error) |
| alert('Export failed. Please try again.') |
| } finally { |
| setIsExporting(false) |
| } |
| } |
|
|
| return ( |
| <> |
| {/* Overlay */} |
| <div |
| className="fixed inset-0 bg-black/60 backdrop-blur-sm z-40 animate-fade-in" |
| onClick={onClose} |
| /> |
| |
| {/* Modal */} |
| <div className="fixed inset-0 z-50 flex items-center justify-center p-4 animate-scale-in"> |
| <div className="glass-lg max-w-2xl w-full rounded-3xl overflow-hidden"> |
| {/* Header */} |
| <div className="flex items-center justify-between p-8 border-b border-white/10"> |
| <h2 className="text-2xl font-bold text-white">Export Clip</h2> |
| <button |
| onClick={onClose} |
| className="p-2 hover:bg-white/10 rounded-lg transition-colors" |
| > |
| <X className="w-5 h-5 text-white/70" /> |
| </button> |
| </div> |
| |
| <div className="p-8 space-y-8"> |
| {/* Format Selection */} |
| <div> |
| <label className="block text-sm font-semibold text-white mb-4">File Format</label> |
| <div className="grid grid-cols-3 gap-3"> |
| {formats.map((fmt) => ( |
| <button |
| key={fmt.id} |
| onClick={() => setFormat(fmt.id)} |
| className={clsx( |
| 'p-4 rounded-xl transition-all text-center', |
| format === fmt.id |
| ? 'glass-lg bg-primary-500/20 border-primary-500/50' |
| : 'glass hover:bg-white/10' |
| )} |
| > |
| <p className="font-bold text-lg text-white">{fmt.label}</p> |
| <p className="text-xs text-white/60 mt-1">{fmt.codec}</p> |
| </button> |
| ))} |
| </div> |
| </div> |
| |
| {/* Quality Selection */} |
| <div> |
| <label className="block text-sm font-semibold text-white mb-4">Quality</label> |
| <div className="space-y-2"> |
| {qualities.map((q) => ( |
| <button |
| key={q.id} |
| onClick={() => setQuality(q.id)} |
| className={clsx( |
| 'w-full p-4 rounded-lg transition-all flex items-center justify-between', |
| quality === q.id |
| ? 'glass-lg bg-primary-500/20 border-primary-500/50' |
| : 'glass hover:bg-white/10' |
| )} |
| > |
| <div className="text-left"> |
| <p className="font-semibold text-white">{q.label}</p> |
| <p className="text-xs text-white/60">{q.size}</p> |
| </div> |
| {quality === q.id && ( |
| <CheckCircle2 className="w-5 h-5 text-primary-500" /> |
| )} |
| </button> |
| ))} |
| </div> |
| </div> |
| |
| {/* Captions */} |
| <div> |
| <label className="block text-sm font-semibold text-white mb-4">Captions</label> |
| <div className="grid grid-cols-3 gap-3"> |
| {[ |
| { id: 'burned', label: 'Burned-In' }, |
| { id: 'srt', label: 'Separate SRT' }, |
| { id: 'none', label: 'None' }, |
| ].map((opt) => ( |
| <button |
| key={opt.id} |
| onClick={() => setCaptions(opt.id)} |
| className={clsx( |
| 'p-4 rounded-xl font-semibold transition-all', |
| captions === opt.id |
| ? 'glass-lg bg-primary-500/20 border-primary-500/50 text-white' |
| : 'glass hover:bg-white/10 text-white/70' |
| )} |
| > |
| {opt.label} |
| </button> |
| ))} |
| </div> |
| </div> |
| |
| {/* Watermark */} |
| <label className="flex items-center gap-4 p-4 glass rounded-xl cursor-pointer hover:bg-white/10 transition-colors"> |
| <input |
| type="checkbox" |
| checked={watermark} |
| onChange={(e) => setWatermark(e.target.checked)} |
| className="w-5 h-5 rounded accent-primary-500 cursor-pointer" |
| /> |
| <div> |
| <p className="font-semibold text-white">Include Watermark</p> |
| <p className="text-sm text-white/60">Adds your brand watermark to the clip</p> |
| </div> |
| </label> |
| |
| {/* File Size Estimate */} |
| <div className="glass-lg p-6 rounded-2xl bg-primary-500/10 border border-primary-500/30"> |
| <div className="flex items-center justify-between"> |
| <div> |
| <p className="text-sm font-semibold text-white">Estimated File Size</p> |
| <p className="text-xs text-white/60 mt-1">Based on selected quality and format</p> |
| </div> |
| <p className="text-2xl font-bold text-primary-500"> |
| {qualities.find((q) => q.id === quality)?.size} |
| </p> |
| </div> |
| </div> |
| |
| {/* Export Complete State */} |
| {exportComplete && ( |
| <div className="glass-lg p-6 rounded-2xl bg-green-500/10 border border-green-500/30 flex items-center gap-4"> |
| <CheckCircle2 className="w-6 h-6 text-green-400 flex-shrink-0" /> |
| <div> |
| <p className="font-semibold text-green-400">Export Complete!</p> |
| <p className="text-sm text-green-400/70">Your clip is downloading...</p> |
| </div> |
| </div> |
| )} |
| |
| {/* Actions */} |
| <div className="flex gap-3 pt-4"> |
| <button |
| onClick={onClose} |
| disabled={isExporting} |
| className="btn-secondary-lg flex-1 disabled:opacity-50" |
| > |
| Cancel |
| </button> |
| <button |
| onClick={handleExport} |
| disabled={isExporting || exportComplete} |
| className="btn-primary-lg flex-1 flex items-center justify-center gap-2 disabled:opacity-50" |
| > |
| {isExporting ? ( |
| <> |
| <div className="w-4 h-4 border-2 border-white/20 border-t-white rounded-full animate-spin" /> |
| Exporting... |
| </> |
| ) : exportComplete ? ( |
| <> |
| <CheckCircle2 className="w-4 h-4" /> |
| Done! |
| </> |
| ) : ( |
| <> |
| <Download className="w-4 h-4" /> |
| Export Clip |
| </> |
| )} |
| </button> |
| </div> |
| </div> |
| </div> |
| </div> |
| </> |
| ) |
| } |
|
|
| export default ExportModal |
|
|