Rovin / frontend /src /components /ShortCard.jsx
Dyen's picture
Deploy Rovin: Dockerized App
0a0a57b
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
// Proper download handler that fetches the file as blob
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)
// Cleanup
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