video-flow-b/components/MyMovies.tsx
2025-10-22 20:24:01 +08:00

125 lines
4.7 KiB
TypeScript

"use client";
import React, { useEffect, useState } from 'react';
import Link from 'next/link';
import { getScriptEpisodeListNew, MovieProject } from '@/api/script_episode';
import { getFirstFrame } from '@/utils/tools';
import { useRouter } from 'next/navigation';
import { motion } from 'framer-motion';
/**
* A lightweight horizontal list of user's movie projects.
* Shows first 9 items, clipped horizontally, with a right-side gradient more button.
*/
const MyMovies: React.FC = () => {
const [projects, setProjects] = useState<MovieProject[]>([]);
const router = useRouter();
const StatusBadge = (status: string): JSX.Element => {
return (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
className="flex items-center"
data-alt="status-badge"
>
{status === 'pending' && (
<>
<motion.span
className="w-2 h-2 rounded-full bg-yellow-400 shadow-[0_0_8px_rgba(255,220,100,0.9)]"
animate={{ scale: [1, 1.4, 1] }}
transition={{ repeat: Infinity, duration: 1.5 }}
/>
</>
)}
{status === 'failed' && (
<>
<motion.span
className="w-2 h-2 rounded-full bg-red-500 shadow-[0_0_8px_rgba(255,0,80,0.9)]"
/>
<span className="ml-1 text-xs tracking-widest text-red-400 font-medium drop-shadow-[0_0_6px_rgba(255,0,80,0.6)]">
FAILED
</span>
</>
)}
</motion.div>
);
};
useEffect(() => {
const user = JSON.parse(localStorage.getItem('currentUser') || '{}');
const userId = String(user.id || '0');
getScriptEpisodeListNew({ user_id: userId, page: 1, per_page: 9 })
.then((res) => {
if ((res as any)?.code === 0) {
setProjects((res as any).data.movie_projects?.slice(0, 9) || []);
}
})
.catch(() => {});
}, []);
return (
<section data-alt="my-movies" className="w-full">
<div data-alt="my-movies-header" className="flex items-center justify-between">
<h2 data-alt="my-movies-title" className="text-xl py-4 font-semibold text-white">My Projects</h2>
<Link data-alt="all-movies-link" href="/movies" className="text-sm px-2 border rounded-full text-blue-400 hover:text-blue-300">All movies </Link>
</div>
<div data-alt="my-movies-row" className="w-full flex items-stretch gap-4">
<div data-alt="movies-scroll" className="flex-1 overflow-hidden relative">
<div data-alt="movies-list" className="flex gap-4">
{projects.map((p) => (
<button
type="button"
key={p.project_id}
data-alt="movie-item"
className="w-48 flex-shrink-0 text-left"
onMouseEnter={(e) => {
const v = e.currentTarget.querySelector('video');
if (v) {
// @ts-ignore
v.play?.();
}
}}
onMouseLeave={(e) => {
const v = e.currentTarget.querySelector('video');
if (v) {
// @ts-ignore
v.pause?.();
// @ts-ignore
v.currentTime = 0;
}
}}
onClick={() => router.push(`/movies/work-flow?episodeId=${p.project_id}`)}
>
<div data-alt="movie-thumb" className="w-48 h-28 rounded-lg overflow-hidden border border-white/10 bg-white/5 relative">
<video
src={p.final_video_url || p.final_simple_video_url || p.video_urls || ''}
className="w-full h-full object-cover object-center"
playsInline
muted
preload="none"
poster={p.video_snapshot_url || getFirstFrame(p.final_video_url || p.final_simple_video_url || p.video_urls || '', 300, p.aspect_ratio)}
/>
<div data-alt="status-overlay" className="absolute top-2 left-2">
{StatusBadge((p.status === 'COMPLETED' || p.final_simple_video_url) ? 'completed' : p.status === 'FAILED' ? 'failed' : 'pending')}
</div>
</div>
<div data-alt="movie-name" className="mt-2 text-sm text-white/90 truncate">{p.name}</div>
</button>
))}
</div>
<div data-alt="mask" className="absolute right-0 top-0 w-20 h-full bg-gradient-to-r from-transparent to-black"></div>
</div>
</div>
</section>
);
};
export default MyMovies;