ViralClipFactory / frontend /src /components /ClipCustomizer.jsx
Alpha123B's picture
Add ClipCustomizer component
761848b verified
Raw
History Blame Contribute Delete
10.3 kB
import { useState } from 'react';
import { motion } from 'framer-motion';
const CAPTION_STYLES = [
{ id: 'heatwave', name: 'HeatWave', emoji: '🔥', desc: 'Yellow + red active, bold energy', colors: ['#FFFF00', '#FF0000'] },
{ id: 'neonpop', name: 'NeonPop', emoji: '💜', desc: 'Cyan glow on dark, bouncy', colors: ['#00FFFF', '#FF00FF'] },
{ id: 'cinematic', name: 'Cinematic', emoji: '🎬', desc: 'White elegance, fade-in', colors: ['#FFFFFF', '#FFD700'] },
{ id: 'hiphop', name: 'HipHop', emoji: '🎤', desc: 'Impact font, slam effect', colors: ['#FFFFFF', '#FF4444'] },
{ id: 'minimal', name: 'Minimal', emoji: '✨', desc: 'Clean white, pure opacity', colors: ['#FFFFFF', '#CCCCCC'] },
];
const VOICE_PRESETS = [
{ id: 'energetic_male', name: 'Energetic Male', emoji: '🔊' },
{ id: 'clear_female', name: 'Clear Female', emoji: '🎙️' },
{ id: 'deep_narrator', name: 'Deep Narrator', emoji: '🎧' },
{ id: 'young_excited', name: 'Young & Excited', emoji: '⚡' },
{ id: 'calm_storyteller', name: 'Calm Storyteller', emoji: '📖' },
];
const MUSIC_GENRES = [
{ id: 'energetic_electronic', name: 'Energetic Electronic', emoji: '🎵' },
{ id: 'lofi_hiphop', name: 'Lo-fi Hip Hop', emoji: '🎶' },
{ id: 'dramatic_orchestral', name: 'Dramatic Orchestral', emoji: '🎻' },
{ id: 'comedic_quirky', name: 'Comedic Quirky', emoji: '🤪' },
{ id: 'suspenseful', name: 'Suspenseful', emoji: '😱' },
];
const COLOR_GRADES = [
{ id: 'teal_orange', name: 'Teal & Orange', color: 'from-teal-500 to-orange-500' },
{ id: 'high_contrast_bw', name: 'High Contrast B&W', color: 'from-gray-900 to-white' },
{ id: 'warm_golden', name: 'Warm Golden', color: 'from-yellow-500 to-orange-600' },
{ id: 'cold_blue', name: 'Cold Blue', color: 'from-blue-700 to-cyan-400' },
{ id: 'vivid_oversaturated', name: 'Vivid', color: 'from-pink-500 to-violet-600' },
];
export default function ClipCustomizer({ candidates, configs, onDone }) {
const [clipConfigs, setClipConfigs] = useState(configs);
const [activeClip, setActiveClip] = useState(0);
const updateConfig = (index, field, value) => {
const updated = [...clipConfigs];
updated[index] = { ...updated[index], [field]: value };
setClipConfigs(updated);
};
const formatTime = (seconds) => {
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${m}:${s.toString().padStart(2, '0')}`;
};
const activeCandidate = candidates[activeClip];
const activeConfig = clipConfigs[activeClip];
if (!activeCandidate || !activeConfig) return null;
return (
<div className="max-w-6xl mx-auto">
<h2 className="text-3xl font-bold mb-2">🎨 Customize Your Clips</h2>
<p className="text-white/50 mb-8">Style each clip with unique captions, voice, music, and color grading</p>
{/* Clip Tabs */}
<div className="flex gap-2 mb-8 overflow-x-auto pb-2">
{candidates.map((candidate, i) => (
<button
key={i}
onClick={() => setActiveClip(i)}
className={`flex-shrink-0 px-4 py-3 rounded-xl text-sm font-medium transition-all
${activeClip === i
? 'bg-primary-500 text-white shadow-lg shadow-primary-500/25'
: 'bg-dark-800 text-white/50 hover:text-white/80 border border-white/5'}`}
>
<span className="font-bold">Clip {i + 1}</span>
<span className="ml-2 text-xs opacity-60">
{formatTime(candidate.start_time)} - {formatTime(candidate.end_time)}
</span>
</button>
))}
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left: Clip Info */}
<div className="card">
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold">📝 Clip Info</h3>
<span className="px-2 py-1 bg-green-500/10 text-green-400 text-xs rounded-full font-medium">
Score: {activeCandidate.virality_score.toFixed(0)}
</span>
</div>
<div className="space-y-4">
<div>
<label className="text-xs text-white/40 uppercase tracking-wider">Title</label>
<input
type="text"
value={activeConfig.title}
onChange={(e) => updateConfig(activeClip, 'title', e.target.value)}
placeholder="AI will generate if empty"
className="w-full mt-1 bg-dark-900 border border-white/10 rounded-lg px-3 py-2 text-sm
focus:outline-none focus:border-primary-500/50"
/>
</div>
<div>
<label className="text-xs text-white/40 uppercase tracking-wider">Duration</label>
<p className="text-sm mt-1">{activeCandidate.duration.toFixed(1)}s</p>
</div>
<div>
<label className="text-xs text-white/40 uppercase tracking-wider">Transcript Preview</label>
<p className="text-xs text-white/60 mt-1 max-h-32 overflow-y-auto leading-relaxed">
{activeCandidate.transcript.slice(0, 300)}...
</p>
</div>
</div>
</div>
{/* Middle: Style Options */}
<div className="card">
<h3 className="font-semibold mb-4">🎨 Caption Style</h3>
<div className="grid grid-cols-1 gap-2 mb-6">
{CAPTION_STYLES.map((style) => (
<button
key={style.id}
onClick={() => updateConfig(activeClip, 'caption_style', style.id)}
className={`flex items-center gap-3 p-3 rounded-lg text-left transition-all text-sm
${activeConfig.caption_style === style.id
? 'bg-primary-500/10 border border-primary-500/30'
: 'bg-dark-900/50 border border-transparent hover:border-white/10'}`}
>
<span className="text-lg">{style.emoji}</span>
<div className="flex-1">
<div className="font-medium">{style.name}</div>
<div className="text-xs text-white/40">{style.desc}</div>
</div>
<div className="flex gap-1">
{style.colors.map((c, i) => (
<div key={i} className="w-3 h-3 rounded-full" style={{ backgroundColor: c }} />
))}
</div>
</button>
))}
</div>
<h3 className="font-semibold mb-3">🎬 Color Grade</h3>
<div className="grid grid-cols-5 gap-2">
{COLOR_GRADES.map((grade) => (
<button
key={grade.id}
onClick={() => updateConfig(activeClip, 'color_grade', grade.id)}
className={`aspect-square rounded-lg bg-gradient-to-br ${grade.color} transition-all
${activeConfig.color_grade === grade.id
? 'ring-2 ring-primary-500 ring-offset-2 ring-offset-dark-800' : 'opacity-60 hover:opacity-100'}`}
title={grade.name}
/>
))}
</div>
</div>
{/* Right: Audio Options */}
<div className="card">
<h3 className="font-semibold mb-4">🔊 Voice</h3>
<div className="space-y-2 mb-6">
{VOICE_PRESETS.map((voice) => (
<button
key={voice.id}
onClick={() => updateConfig(activeClip, 'voice_preset', voice.id)}
className={`w-full flex items-center gap-3 p-3 rounded-lg text-left text-sm transition-all
${activeConfig.voice_preset === voice.id
? 'bg-primary-500/10 border border-primary-500/30'
: 'bg-dark-900/50 border border-transparent hover:border-white/10'}`}
>
<span>{voice.emoji}</span>
<span className="font-medium">{voice.name}</span>
</button>
))}
</div>
<h3 className="font-semibold mb-3">🎵 Background Music</h3>
<div className="space-y-2 mb-6">
{MUSIC_GENRES.map((genre) => (
<button
key={genre.id}
onClick={() => updateConfig(activeClip, 'music_genre', genre.id)}
className={`w-full flex items-center gap-3 p-2 rounded-lg text-left text-sm transition-all
${activeConfig.music_genre === genre.id
? 'bg-primary-500/10 border border-primary-500/30'
: 'bg-dark-900/50 border border-transparent hover:border-white/10'}`}
>
<span>{genre.emoji}</span>
<span className="font-medium">{genre.name}</span>
</button>
))}
</div>
{/* Toggles */}
<div className="space-y-3 pt-4 border-t border-white/5">
<label className="flex items-center justify-between cursor-pointer">
<span className="text-sm">Include original audio</span>
<input
type="checkbox"
checked={activeConfig.include_original_audio}
onChange={(e) => updateConfig(activeClip, 'include_original_audio', e.target.checked)}
className="w-4 h-4 rounded accent-primary-500"
/>
</label>
<label className="flex items-center justify-between cursor-pointer">
<span className="text-sm">Include stock images</span>
<input
type="checkbox"
checked={activeConfig.include_stock_images}
onChange={(e) => updateConfig(activeClip, 'include_stock_images', e.target.checked)}
className="w-4 h-4 rounded accent-primary-500"
/>
</label>
</div>
</div>
</div>
{/* Generate Button */}
<div className="text-center mt-10">
<button
onClick={() => onDone(clipConfigs)}
className="btn-primary text-lg px-12 py-4"
>
⚡ Generate {candidates.length} Viral Clips
</button>
</div>
</div>
);
}