| 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);
|
|
|
|
|
| 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... 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">>_</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>
|
| );
|
| }
|
|
|