| import { useEffect, useState } from 'react'; | |
| import { useParams, Link, useNavigate } from 'react-router-dom'; | |
| import { useDispatch, useSelector } from 'react-redux'; | |
| import { motion } from 'framer-motion'; | |
| import { AppDispatch, RootState } from '../store'; | |
| import { fetchProject } from '../store/projectsSlice'; | |
| import AITerminal from '../components/AITerminal'; | |
| export default function ProjectPage() { | |
| const { id } = useParams<{ id: string }>(); | |
| const dispatch = useDispatch<AppDispatch>(); | |
| const { current: project } = useSelector((state: RootState) => state.projects); | |
| const [showTerminal, setShowTerminal] = useState(false); | |
| const navigate = useNavigate(); | |
| useEffect(() => { | |
| if (id) dispatch(fetchProject(id)); | |
| }, [id, dispatch]); | |
| if (!project) { | |
| return ( | |
| <div className="min-h-screen pt-20 flex items-center justify-center"> | |
| <div className="w-8 h-8 border-2 border-gold-500 border-t-transparent rounded-full animate-spin" /> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div className="min-h-screen pt-20 pb-12 px-6"> | |
| <div className="max-w-6xl mx-auto"> | |
| {/* Breadcrumb */} | |
| <div className="flex items-center gap-2 text-sm text-light-500 mb-6"> | |
| <Link to="/dashboard" className="hover:text-gold-500 transition-colors">Dashboard</Link> | |
| <span>/</span> | |
| <span className="text-light-300">{project.name}</span> | |
| </div> | |
| {/* Header */} | |
| <motion.div | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8" | |
| > | |
| <div> | |
| <h1 className="font-display text-3xl font-bold text-light-100 mb-1">{project.name}</h1> | |
| <p className="text-light-500"> | |
| {project.defaultPlatform} -- {project.defaultFormat} -- {project.videos?.length || 0} videos | |
| </p> | |
| </div> | |
| <div className="flex gap-3"> | |
| <button onClick={() => setShowTerminal(!showTerminal)} className="btn-ghost text-sm"> | |
| <span className="mr-1 font-mono">>_</span> Terminal | |
| </button> | |
| <button | |
| onClick={() => navigate(`/project/${id}/create`)} | |
| className="btn-primary" | |
| id="create-video-btn" | |
| > | |
| Create Video | |
| </button> | |
| </div> | |
| </motion.div> | |
| {/* Terminal */} | |
| {showTerminal && ( | |
| <motion.div | |
| initial={{ opacity: 0, height: 0 }} | |
| animate={{ opacity: 1, height: 'auto' }} | |
| className="mb-8 overflow-hidden" | |
| > | |
| <AITerminal /> | |
| </motion.div> | |
| )} | |
| {/* Videos */} | |
| {(!project.videos || project.videos.length === 0) ? ( | |
| <motion.div | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| className="card-static text-center py-16" | |
| > | |
| <h3 className="font-display text-xl text-light-100 mb-2">No videos yet</h3> | |
| <p className="text-light-500 mb-6">Create your first video in this project</p> | |
| <button onClick={() => navigate(`/project/${id}/create`)} className="btn-primary"> | |
| Create First Video | |
| </button> | |
| </motion.div> | |
| ) : ( | |
| <div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6"> | |
| {project.videos.map((video: any, i: number) => ( | |
| <motion.div | |
| key={video._id || i} | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ delay: i * 0.05 }} | |
| > | |
| <Link to={`/video/${video._id}/preview`} className="card block group"> | |
| <div className="flex items-center justify-between mb-3"> | |
| <span className={`text-xs px-2 py-1 rounded font-medium ${video.status === 'exported' ? 'bg-green-500/10 text-green-400' : | |
| video.status === 'generating' ? 'bg-gold-500/10 text-gold-500' : | |
| video.status === 'failed' ? 'bg-red-500/10 text-red-400' : | |
| 'bg-dark-500 text-light-500' | |
| }`}> | |
| {video.status || 'pending'} | |
| </span> | |
| </div> | |
| <p className="text-sm text-light-400 line-clamp-3"> | |
| {video.script?.slice(0, 120) || 'No script yet'}... | |
| </p> | |
| </Link> | |
| </motion.div> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |