director-ai / client /src /pages /VideoCreate.tsx
algorembrant's picture
Upload 79 files
11f4e50 verified
import { useState, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { motion, AnimatePresence } from 'framer-motion';
import { AppDispatch } from '../store';
import { createVideo } from '../store/videosSlice';
import AITerminal from '../components/AITerminal';
const STEPS = [
{ num: 1, title: 'Script', desc: 'Paste or write your video script' },
{ num: 2, title: 'Voice', desc: 'Choose voice settings' },
{ num: 3, title: 'Music', desc: 'Select background music' },
{ num: 4, title: 'Style', desc: 'Set visual preferences' },
{ num: 5, title: 'Assets', desc: 'Upload media files' },
];
export default function VideoCreate() {
const { projectId } = useParams<{ projectId: string }>();
const dispatch = useDispatch<AppDispatch>();
const navigate = useNavigate();
const [step, setStep] = useState(1);
const [showTerminal, setShowTerminal] = useState(false);
const [submitting, setSubmitting] = useState(false);
// Form state
const [script, setScript] = useState('');
const [voice, setVoice] = useState({
type: 'neutral',
language: 'en',
tone: 'professional',
speed: 1.0,
});
const [music, setMusic] = useState('');
const [style, setStyle] = useState({
fonts: { primary: 'Playfair Display', secondary: 'Montserrat' },
colors: { primary: '#1A1A1A', secondary: '#F5F5F5', accent: '#D4AF37' },
transitions: 'fade',
videoStyle: 'minimal',
});
const [files, setFiles] = useState<File[]>([]);
const next = () => setStep((s) => Math.min(s + 1, 5));
const prev = () => setStep((s) => Math.max(s - 1, 1));
const handleFileChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
setFiles(Array.from(e.target.files));
}
}, []);
const handleGenerate = async () => {
if (!projectId || !script.trim()) return;
setSubmitting(true);
const videoData = {
script,
voice,
music: { filePath: music },
assets: files.map((f) => ({ type: 'clip' as const, path: f.name })),
};
const result = await dispatch(createVideo({ projectId, data: videoData }));
setSubmitting(false);
if (createVideo.fulfilled.match(result)) {
navigate(`/video/${result.payload._id}/preview`);
}
};
return (
<div className="min-h-screen pt-20 pb-12 px-6">
<div className="max-w-4xl mx-auto">
{/* Stepper */}
<div className="flex items-center justify-between mb-12 relative">
<div className="absolute top-5 left-0 right-0 h-px bg-dark-400/30" />
{STEPS.map((s) => (
<div
key={s.num}
className="relative flex flex-col items-center cursor-pointer z-10"
onClick={() => setStep(s.num)}
>
<div className={`w-10 h-10 rounded-full flex items-center justify-center font-body font-semibold text-sm transition-all duration-300 ${step === s.num
? 'bg-gold-500 text-dark-900 shadow-gold'
: step > s.num
? 'bg-gold-500/20 text-gold-500 border border-gold-500/40'
: 'bg-dark-600 text-light-500 border border-dark-400/30'
}`}>
{step > s.num ? (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
) : s.num}
</div>
<span className={`mt-2 text-xs font-body transition-colors ${step === s.num ? 'text-gold-500' : 'text-light-500'
}`}>
{s.title}
</span>
</div>
))}
</div>
{/* Step Content */}
<AnimatePresence mode="wait">
<motion.div
key={step}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3 }}
className="glass-panel p-8 mb-8"
>
{step === 1 && (
<div>
<h2 className="font-display text-2xl font-bold text-light-100 mb-2">Your Script</h2>
<p className="text-light-500 mb-6">Paste your video script or write it directly. This will be used for voiceover and subtitles.</p>
<textarea
value={script}
onChange={(e) => setScript(e.target.value)}
className="input-field h-48 resize-none"
placeholder="Paste your video script here...&#10;&#10;Example: Did you know that 90% of successful content creators use faceless videos? Here is why this strategy works and how you can start today..."
id="script-input"
/>
<p className="mt-2 text-xs text-light-500/60">{script.length} characters</p>
</div>
)}
{step === 2 && (
<div>
<h2 className="font-display text-2xl font-bold text-light-100 mb-2">Voice Settings</h2>
<p className="text-light-500 mb-6">Configure the AI voiceover for your video.</p>
<div className="grid sm:grid-cols-2 gap-5">
<div>
<label className="block text-sm text-light-400 mb-2">Voice Type</label>
<select value={voice.type} onChange={(e) => setVoice({ ...voice, type: e.target.value })} className="input-field" id="voice-type">
<option value="neutral">Neutral</option>
<option value="male">Male</option>
<option value="female">Female</option>
<option value="deep">Deep</option>
<option value="energetic">Energetic</option>
</select>
</div>
<div>
<label className="block text-sm text-light-400 mb-2">Language</label>
<select value={voice.language} onChange={(e) => setVoice({ ...voice, language: e.target.value })} className="input-field" id="voice-language">
<option value="en">English</option>
<option value="es">Spanish</option>
<option value="fr">French</option>
<option value="de">German</option>
<option value="pt">Portuguese</option>
<option value="ja">Japanese</option>
</select>
</div>
<div>
<label className="block text-sm text-light-400 mb-2">Tone</label>
<select value={voice.tone} onChange={(e) => setVoice({ ...voice, tone: e.target.value })} className="input-field" id="voice-tone">
<option value="professional">Professional</option>
<option value="casual">Casual</option>
<option value="dramatic">Dramatic</option>
<option value="upbeat">Upbeat</option>
<option value="calm">Calm</option>
</select>
</div>
<div>
<label className="block text-sm text-light-400 mb-2">Speed: {voice.speed}x</label>
<input
type="range"
min="0.5"
max="2.0"
step="0.1"
value={voice.speed}
onChange={(e) => setVoice({ ...voice, speed: parseFloat(e.target.value) })}
className="w-full accent-gold-500"
id="voice-speed"
/>
</div>
</div>
</div>
)}
{step === 3 && (
<div>
<h2 className="font-display text-2xl font-bold text-light-100 mb-2">Background Music</h2>
<p className="text-light-500 mb-6">Choose music for your video or upload your own track.</p>
<div className="space-y-3">
{['None', 'Ambient Calm', 'Upbeat Energy', 'Corporate', 'Cinematic', 'Lo-Fi Chill'].map((track) => (
<label
key={track}
className={`card-static flex items-center gap-4 cursor-pointer transition-all ${music === track ? 'border-gold-500/40 shadow-gold' : ''
}`}
>
<input
type="radio"
name="music"
value={track}
checked={music === track}
onChange={(e) => setMusic(e.target.value)}
className="accent-gold-500"
/>
<span className="text-light-300">{track}</span>
</label>
))}
</div>
</div>
)}
{step === 4 && (
<div>
<h2 className="font-display text-2xl font-bold text-light-100 mb-2">Visual Style</h2>
<p className="text-light-500 mb-6">Customize the look and feel of your video.</p>
<div className="grid sm:grid-cols-2 gap-5">
<div>
<label className="block text-sm text-light-400 mb-2">Video Style</label>
<select
value={style.videoStyle}
onChange={(e) => setStyle({ ...style, videoStyle: e.target.value })}
className="input-field"
id="video-style"
>
<option value="minimal">Minimal</option>
<option value="dynamic">Dynamic</option>
<option value="cinematic">Cinematic</option>
<option value="bold">Bold</option>
<option value="elegant">Elegant</option>
</select>
</div>
<div>
<label className="block text-sm text-light-400 mb-2">Transitions</label>
<select
value={style.transitions}
onChange={(e) => setStyle({ ...style, transitions: e.target.value })}
className="input-field"
id="transitions"
>
<option value="fade">Fade</option>
<option value="slide">Slide</option>
<option value="zoom">Zoom</option>
<option value="dissolve">Dissolve</option>
<option value="none">None</option>
</select>
</div>
<div>
<label className="block text-sm text-light-400 mb-2">Accent Color</label>
<div className="flex items-center gap-3">
<input
type="color"
value={style.colors.accent}
onChange={(e) => setStyle({ ...style, colors: { ...style.colors, accent: e.target.value } })}
className="w-10 h-10 rounded border border-dark-400/30 cursor-pointer"
/>
<span className="text-sm text-light-500 font-mono">{style.colors.accent}</span>
</div>
</div>
<div>
<label className="block text-sm text-light-400 mb-2">Headline Font</label>
<select
value={style.fonts.primary}
onChange={(e) => setStyle({ ...style, fonts: { ...style.fonts, primary: e.target.value } })}
className="input-field"
>
<option value="Playfair Display">Playfair Display</option>
<option value="Montserrat">Montserrat</option>
<option value="Inter">Inter</option>
<option value="Roboto">Roboto</option>
<option value="Lora">Lora</option>
</select>
</div>
</div>
</div>
)}
{step === 5 && (
<div>
<h2 className="font-display text-2xl font-bold text-light-100 mb-2">Upload Assets</h2>
<p className="text-light-500 mb-6">Add clips, images, or logos to include in your video.</p>
<div className="border-2 border-dashed border-dark-400/50 rounded-xl p-8 text-center hover:border-gold-500/40 transition-colors">
<input
type="file"
multiple
onChange={handleFileChange}
className="hidden"
id="asset-upload"
accept="image/*,video/*,audio/*"
/>
<label htmlFor="asset-upload" className="cursor-pointer">
<div className="w-12 h-12 bg-gold-500/10 border border-gold-500/20 rounded-xl flex items-center justify-center mx-auto mb-4">
<span className="text-gold-500 text-2xl">+</span>
</div>
<p className="text-light-300 font-medium mb-1">Drag and drop or click to upload</p>
<p className="text-sm text-light-500">Images, videos, logos, and audio (max 100MB each)</p>
</label>
</div>
{files.length > 0 && (
<div className="mt-4 space-y-2">
{files.map((file, i) => (
<div key={i} className="flex items-center justify-between card-static py-3">
<span className="text-sm text-light-300">{file.name}</span>
<span className="text-xs text-light-500">{(file.size / 1024 / 1024).toFixed(1)} MB</span>
</div>
))}
</div>
)}
</div>
)}
</motion.div>
</AnimatePresence>
{/* Navigation + Terminal Toggle */}
<div className="flex items-center justify-between">
<button
onClick={prev}
disabled={step === 1}
className="btn-ghost disabled:opacity-30"
>
Previous
</button>
<button
onClick={() => setShowTerminal(!showTerminal)}
className="btn-ghost text-sm"
>
<span className="mr-1 font-mono">&gt;_</span> Terminal
</button>
{step < 5 ? (
<button onClick={next} className="btn-primary">
Next Step
</button>
) : (
<button
onClick={handleGenerate}
disabled={submitting || !script.trim()}
className="btn-primary px-10"
id="generate-btn"
>
{submitting ? 'Generating...' : 'Generate Preview'}
</button>
)}
</div>
{/* Terminal Panel */}
{showTerminal && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
className="mt-8 overflow-hidden"
>
<AITerminal />
</motion.div>
)}
</div>
</div>
);
}