clipon / frontend /src /components /ClipsGallery.jsx
yonagush
Fix: 404 job polling, stale closure, brand kit UI, article→reels feature
b5d56b9
import { useState } from 'react'
import { Download, Plus, TrendingUp, Clock, Tag, Film, ExternalLink } from 'lucide-react'
import clsx from 'clsx'
import ClipCard from './ClipCard'
/**
* Clips gallery — handles both video clip jobs AND article reel jobs.
* Props: { jobResult, jobId, jobType, onReset }
*/
function ClipsGallery({ jobResult, jobId, jobType = 'clip', onReset }) {
const [sortBy, setSortBy] = useState('score')
const [selectedTopic, setSelectedTopic] = useState(null)
// ── ARTICLE REEL result ──────────────────────────────────────────────────
if (jobType === 'article') {
return (
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-10">
<h2 className="text-4xl font-bold text-white mb-2">Your Article Reel is Ready!</h2>
<p className="text-white/60">Complete with AI voiceover and chainstreet.io CTA</p>
</div>
<div className="glass-lg p-8 rounded-3xl mb-8">
{/* Script preview */}
{jobResult?.sentences?.length > 0 && (
<div className="mb-8">
<h3 className="text-sm font-semibold text-white/60 uppercase tracking-wider mb-4">Script</h3>
<div className="space-y-3">
{jobResult.sentences.map((line, i) => (
<div key={i} className="flex gap-3 p-3 rounded-xl bg-white/5">
<span className="text-primary-500 font-bold text-sm w-6 shrink-0">{i + 1}</span>
<p className="text-white text-sm">{line}</p>
</div>
))}
</div>
</div>
)}
<div className="flex flex-col sm:flex-row gap-4 mt-6">
<a
href={`/api/article-download/${jobId}`}
download
className="btn-primary-lg flex items-center gap-2 justify-center flex-1"
>
<Download className="w-4 h-4" />
Download Reel (MP4)
</a>
<button onClick={onReset} className="btn-secondary-lg flex items-center gap-2 justify-center flex-1">
<Plus className="w-4 h-4" />
New Reel
</button>
</div>
</div>
<div className="glass p-5 rounded-xl flex items-start gap-3">
<ExternalLink className="w-4 h-4 text-accent-500 mt-0.5 shrink-0" />
<p className="text-sm text-white/70">
This reel includes a "Read the full story at chainstreet dot i o" call-to-action — perfect for driving traffic from social media back to your article.
</p>
</div>
</div>
)
}
// ── VIDEO CLIPS result ───────────────────────────────────────────────────
if (!jobResult || !jobResult.clips) {
return (
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="glass-lg p-12 rounded-3xl text-center">
<Film className="w-12 h-12 text-white/30 mx-auto mb-4" />
<p className="text-white/60 mb-4">No clips generated yet</p>
<button onClick={onReset} className="btn-primary-lg">Start a New Project</button>
</div>
</div>
)
}
const clips = jobResult.clips || []
const allTopics = [...new Set(clips.flatMap((c) => c.topics || []))]
// Filter clips by topic
let filteredClips = clips
if (selectedTopic) {
filteredClips = clips.filter((c) => c.topics?.includes(selectedTopic))
}
// Sort clips
const sortedClips = [...filteredClips].sort((a, b) => {
if (sortBy === 'score') {
return (b.virality_score || 0) - (a.virality_score || 0)
} else if (sortBy === 'duration') {
return (b.duration || 0) - (a.duration || 0)
} else if (sortBy === 'match') {
return (b.match_score || 0) - (a.match_score || 0)
}
return 0
})
const downloadAllClips = () => {
// Mock implementation - would batch download all clips
alert('Downloading all clips...')
}
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-8">
<div>
<h2 className="text-4xl font-bold text-white mb-2">
{clips.length} Viral Clips Ready
</h2>
<p className="text-white/60">
Pick your favorites and download them instantly
</p>
</div>
<button
onClick={downloadAllClips}
className="btn-primary-lg flex items-center gap-2 w-fit"
>
<Download className="w-4 h-4" />
Export All
</button>
</div>
{/* Sorting & Filtering */}
<div className="glass-lg p-6 rounded-2xl mb-8">
<div className="flex flex-col md:flex-row md:items-center gap-6">
{/* Sort Tabs */}
<div className="flex gap-2">
<button
onClick={() => setSortBy('score')}
className={clsx(
'flex items-center gap-2 px-4 py-2 rounded-lg font-semibold transition-all',
sortBy === 'score'
? 'bg-primary-500 text-white shadow-glow'
: 'bg-white/10 text-white/70 hover:bg-white/20'
)}
>
<TrendingUp className="w-4 h-4" />
Highest Score
</button>
<button
onClick={() => setSortBy('match')}
className={clsx(
'flex items-center gap-2 px-4 py-2 rounded-lg font-semibold transition-all',
sortBy === 'match'
? 'bg-primary-500 text-white shadow-glow'
: 'bg-white/10 text-white/70 hover:bg-white/20'
)}
>
<TrendingUp className="w-4 h-4" />
Best Match
</button>
<button
onClick={() => setSortBy('duration')}
className={clsx(
'flex items-center gap-2 px-4 py-2 rounded-lg font-semibold transition-all',
sortBy === 'duration'
? 'bg-primary-500 text-white shadow-glow'
: 'bg-white/10 text-white/70 hover:bg-white/20'
)}
>
<Clock className="w-4 h-4" />
Longest
</button>
</div>
{/* Topic Filter */}
{allTopics.length > 0 && (
<div className="flex flex-wrap gap-2">
<button
onClick={() => setSelectedTopic(null)}
className={clsx(
'px-3 py-2 rounded-lg font-semibold text-sm transition-all',
selectedTopic === null
? 'bg-primary-500 text-white shadow-glow'
: 'bg-white/10 text-white/70 hover:bg-white/20'
)}
>
All Topics
</button>
{allTopics.map((topic) => (
<button
key={topic}
onClick={() => setSelectedTopic(topic)}
className={clsx(
'px-3 py-2 rounded-lg font-semibold text-sm transition-all flex items-center gap-1',
selectedTopic === topic
? 'bg-accent-500 text-white shadow-glow-pink'
: 'bg-white/10 text-white/70 hover:bg-white/20'
)}
>
<Tag className="w-3 h-3" />
{topic}
</button>
))}
</div>
)}
</div>
</div>
{/* Clips Grid */}
{sortedClips.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-12">
{sortedClips.map((clip, idx) => (
<ClipCard key={clip.id || idx} clip={clip} jobId={jobId} index={idx} />
))}
</div>
) : (
<div className="glass-lg p-12 rounded-3xl text-center mb-12">
<p className="text-white/60 mb-4">No clips match your filters</p>
<button
onClick={() => setSelectedTopic(null)}
className="btn-secondary"
>
Clear Filters
</button>
</div>
)}
{/* Bottom CTA */}
<div className="glass-lg p-8 rounded-3xl">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h3 className="text-xl font-bold text-white mb-1">Need more clips?</h3>
<p className="text-white/60">
Adjust settings and regenerate with different parameters
</p>
</div>
<button
onClick={onReset}
className="btn-primary-lg flex items-center gap-2 w-fit"
>
<Plus className="w-4 h-4" />
New Project
</button>
</div>
</div>
</div>
)
}
export default ClipsGallery