clipon / frontend /src /components /ExportModal.jsx
yonagush
Launch Clipon — AI-powered viral clip generator
edb7d3e
import { useState } from 'react'
import { X, Download, CheckCircle2 } from 'lucide-react'
import clsx from 'clsx'
import { downloadClip } from '../utils/api'
/**
* Export modal for downloading clips
* Props: { isOpen, onClose, clip, jobId }
*/
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,
})
// Create download link
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