| import { useState } from 'react' |
|
|
| function ShortCard({ short, jobId, apiBase }) { |
| const [downloading, setDownloading] = useState({}) |
|
|
| const formatDuration = (seconds) => { |
| const mins = Math.floor(seconds / 60) |
| const secs = Math.floor(seconds % 60) |
| return `${mins}:${secs.toString().padStart(2, '0')}` |
| } |
|
|
| const isReady = short.status === 'ready' |
|
|
| const videoUrl = `${apiBase}/jobs/${jobId}/shorts/${short.short_id}/video` |
| const srtUrl = `${apiBase}/jobs/${jobId}/shorts/${short.short_id}/srt` |
| const subtitledUrl = `${apiBase}/jobs/${jobId}/shorts/${short.short_id}/subtitled` |
| const metadataUrl = `${apiBase}/jobs/${jobId}/shorts/${short.short_id}/metadata` |
| const thumbnailUrl = short.thumbnail_url |
| ? `${apiBase}${short.thumbnail_url}` |
| : null |
|
|
| |
| const handleDownload = async (url, filename, type) => { |
| if (!isReady || downloading[type]) return |
|
|
| setDownloading(prev => ({ ...prev, [type]: true })) |
|
|
| try { |
| const response = await fetch(url) |
|
|
| if (!response.ok) { |
| throw new Error(`Download failed: ${response.status}`) |
| } |
|
|
| const blob = await response.blob() |
| const blobUrl = window.URL.createObjectURL(blob) |
|
|
| const link = document.createElement('a') |
| link.href = blobUrl |
| link.download = filename |
| document.body.appendChild(link) |
| link.click() |
| document.body.removeChild(link) |
|
|
| |
| setTimeout(() => window.URL.revokeObjectURL(blobUrl), 100) |
| } catch (error) { |
| console.error('Download error:', error) |
| alert(`Download failed: ${error.message}`) |
| } finally { |
| setDownloading(prev => ({ ...prev, [type]: false })) |
| } |
| } |
|
|
| return ( |
| <div className="short-card"> |
| <div className="short-thumbnail"> |
| {thumbnailUrl ? ( |
| <img src={thumbnailUrl} alt={short.tagline} /> |
| ) : ( |
| <div className="short-thumbnail-placeholder" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '10px' }}> |
| <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round"> |
| <rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"></rect> |
| <line x1="7" y1="2" x2="7" y2="22"></line> |
| <line x1="17" y1="2" x2="17" y2="22"></line> |
| <line x1="2" y1="12" x2="22" y2="12"></line> |
| <line x1="2" y1="7" x2="7" y2="7"></line> |
| <line x1="2" y1="17" x2="7" y2="17"></line> |
| <line x1="17" y1="17" x2="22" y2="17"></line> |
| <line x1="17" y1="7" x2="22" y2="7"></line> |
| </svg> |
| </div> |
| )} |
| |
| <span className={`short-status-badge ${short.status}`}> |
| {short.status} |
| </span> |
| |
| <span className="short-duration"> |
| {formatDuration(short.duration)} |
| </span> |
| </div> |
| |
| <div className="short-content"> |
| <p className="short-tagline" style={{ fontWeight: 600, fontSize: '1rem', lineHeight: '1.4' }}>{short.tagline}</p> |
| |
| <div className="short-meta" style={{ display: 'flex', alignItems: 'center', gap: '12px', marginTop: '8px', marginBottom: '16px' }}> |
| <span className="short-score" style={{ display: 'flex', alignItems: 'center', gap: '4px', fontWeight: 500, color: 'var(--text-primary)' }}> |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="var(--accent-primary)" stroke="none"> |
| <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon> |
| </svg> |
| {short.score.toFixed(1)} / 10 |
| </span> |
| <span style={{ color: 'var(--text-muted)' }}>•</span> |
| <span style={{ color: 'var(--text-secondary)' }}> |
| {formatDuration(short.start_time)} - {formatDuration(short.end_time)} |
| </span> |
| </div> |
| |
| <div className="short-actions" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px' }}> |
| <button |
| onClick={() => handleDownload(videoUrl, `${short.short_id}.mp4`, 'video')} |
| className={`download-btn primary ${!isReady ? 'disabled' : ''}`} |
| disabled={!isReady || downloading.video} |
| style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px' }} |
| > |
| {downloading.video ? ( |
| <div className="spinner" style={{ width: '12px', height: '12px', border: '2px solid rgba(255,255,255,0.3)', borderTopColor: 'white' }}></div> |
| ) : ( |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg> |
| )} |
| video |
| </button> |
| <button |
| onClick={() => handleDownload(srtUrl, `${short.short_id}.srt`, 'srt')} |
| className={`download-btn ${!isReady ? 'disabled' : ''}`} |
| disabled={!isReady || downloading.srt} |
| style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px' }} |
| > |
| {downloading.srt ? ( |
| <div className="spinner" style={{ width: '12px', height: '12px', border: '2px solid rgba(255,255,255,0.3)', borderTopColor: 'white' }}></div> |
| ) : ( |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg> |
| )} |
| subs |
| </button> |
| <button |
| onClick={() => handleDownload(subtitledUrl, `${short.short_id}_captioned.mp4`, 'subtitled')} |
| className={`download-btn ${!isReady ? 'disabled' : ''}`} |
| disabled={!isReady || downloading.subtitled} |
| style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px' }} |
| > |
| {downloading.subtitled ? ( |
| <div className="spinner" style={{ width: '12px', height: '12px', border: '2px solid rgba(255,255,255,0.3)', borderTopColor: 'white' }}></div> |
| ) : ( |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polygon points="23 7 16 12 23 17 23 7"></polygon><rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect></svg> |
| )} |
| captioned |
| </button> |
| <button |
| onClick={() => handleDownload(metadataUrl, `${short.short_id}_metadata.txt`, 'metadata')} |
| className={`download-btn ${!isReady ? 'disabled' : ''}`} |
| disabled={!isReady || downloading.metadata} |
| style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px', color: 'var(--text-primary)' }} |
| title="Download metadata" |
| > |
| {downloading.metadata ? ( |
| <div className="spinner" style={{ width: '12px', height: '12px', border: '2px solid rgba(255,255,255,0.3)', borderTopColor: 'white' }}></div> |
| ) : ( |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect></svg> |
| )} |
| meta |
| </button> |
| </div> |
| </div> |
| </div> |
| ) |
| } |
|
|
| export default ShortCard |
|
|