| import { useState } from 'react' |
| import { Download, Plus, TrendingUp, Clock, Tag, Film, ExternalLink } from 'lucide-react' |
| import clsx from 'clsx' |
| import ClipCard from './ClipCard' |
|
|
| |
| |
| |
| |
| function ClipsGallery({ jobResult, jobId, jobType = 'clip', onReset }) { |
| const [sortBy, setSortBy] = useState('score') |
| const [selectedTopic, setSelectedTopic] = useState(null) |
|
|
| |
| 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> |
| ) |
| } |
|
|
| |
| 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 || []))] |
|
|
| |
| let filteredClips = clips |
| if (selectedTopic) { |
| filteredClips = clips.filter((c) => c.topics?.includes(selectedTopic)) |
| } |
|
|
| |
| 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 = () => { |
| |
| 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 |
|
|