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

207 lines
8.8 KiB
TypeScript

"use client"
import type React from "react"
import { useEffect, useState } from "react"
import { X } from "lucide-react"
import TemplatePreviewModal from "@/components/common/TemplatePreviewModal"
import { PcTemplateModal } from "@/components/ChatInputBox/PcTemplateModal"
import { useTemplateStoryServiceHook } from "@/app/service/Interaction/templateStoryService"
import { useAppDispatch } from "@/lib/store/hooks"
import { selectTemplateById } from "@/lib/store/creationTemplateSlice"
import { useDeviceType } from "@/hooks/useDeviceType"
/**
* A compact template showcase with a header and link to all templates.
* Shows first 12 templates, in 3 columns, each with thumbnail, name and brief.
* @param {object} props - Component props.
* @param {boolean} [props.showTabs=true] - Whether to show category tabs.
* @returns {JSX.Element} - FamousTemplate component
*/
interface FamousTemplateProps { showTabs?: boolean }
const FamousTemplate: React.FC<FamousTemplateProps> = ({ showTabs = true }) => {
const { templateStoryList, getTemplateStoryList, isLoading } = useTemplateStoryServiceHook()
const dispatch = useAppDispatch()
const { isDesktop } = useDeviceType()
const [isModalOpen, setIsModalOpen] = useState(false)
const [initialTemplateId, setInitialTemplateId] = useState<string | undefined>(undefined)
const [isTemplateCreating, setIsTemplateCreating] = useState(false)
const [isRoleGenerating, setIsRoleGenerating] = useState<{ [key: string]: boolean }>({})
const [isItemGenerating, setIsItemGenerating] = useState<{ [key: string]: boolean }>({})
const [activeTemplateId, setActiveTemplateId] = useState<string | null>(null)
const [isPreviewReady, setIsPreviewReady] = useState(false)
const [activeTab, setActiveTab] = useState<"all" | "music" | "animation" | "fantasy">("all")
useEffect(() => {
void getTemplateStoryList()
}, [getTemplateStoryList])
const filteredTemplates = templateStoryList.filter((t) => {
if (activeTab === "all") return true
const categories = (t.category || "").split(",").map((s) => s.trim().toLowerCase())
return categories.includes(activeTab)
}).slice(0, 10)
const topTemplates = filteredTemplates
return (
<section data-alt="famous-template" className="w-full">
<div data-alt="famous-template-header" className={`items-center ${isDesktop ? 'flex' : 'block'}`}>
<h2 data-alt="famous-template-title" className="text-xl py-4 font-semibold text-white">
Inspiration Lab
</h2>
{showTabs && (
<div data-alt="template-tabs" className={`flex flex-wrap items-center gap-2 ${isDesktop ? 'ml-4' : 'ml-0'}`}>
{(["all", "music", "animation", "fantasy"] as const).map((tab) => (
<button
key={tab}
type="button"
data-alt={`template-tab-${tab}`}
onClick={() => setActiveTab(tab)}
className={`px-3 py-0.5 rounded-none text-sm transition-colors border ${
activeTab === tab
? "border-white/60 bg-white/80 text-slate-900"
: "border-white/20 text-white/80 hover:border-white/40 hover:bg-white/10"
}`}
>
{tab.toUpperCase()}
</button>
))}
</div>
)}
</div>
{isLoading ? (
<div data-alt="loading" className="text-sm text-white/70">
Loading...
</div>
) : (
<div data-alt="template-grid" className="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-5 gap-4 lg:gap-8">
{topTemplates.map((t) => {
return (
<div
data-alt="template-item"
key={t.id}
className="relative h-40 group [container-type:inline-size]"
onMouseEnter={(e) => {
const v = e.currentTarget.querySelector('video') as HTMLVideoElement | null
v?.play?.()
}}
onMouseLeave={(e) => {
const v = e.currentTarget.querySelector('video') as HTMLVideoElement | null
v?.pause?.()
if (v) {
v.currentTime = 0
}
}}
>
<div
data-alt="template-item-placeholder"
className="flex items-center gap-3 rounded-lg border border-white/10 bg-white/0 h-full transition-transform duration-300 ease-out group-hover:scale-105"
>
<div
data-alt="template-thumb"
className="w-full h-full rounded-md overflow-hidden border border-white/10 flex-shrink-0 relative"
>
<img
src={t.image_url?.[0] || ""}
alt={t.name}
className="w-full h-full object-cover object-center"
/>
<div data-alt="template-meta" className="flex-1 min-w-0 absolute bg-black/50 bottom-0 left-0 right-0 px-3 py-3">
<div data-alt="template-name" className="text-base font-bold text-white truncate">{t.name}</div>
</div>
<div data-alt="template-video" className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300 ease-out">
{t.show_url && <video
src={t.show_url}
className="w-full h-full object-cover object-center"
playsInline
muted
preload="none"
/>}
</div>
<div
data-alt="template-hover-overlay"
className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-300 ease-out pointer-events-none z-10"
/>
</div>
</div>
<div
data-alt="template-item-click-layer"
className="absolute inset-0 cursor-pointer"
onClick={() => {
const id = t.id || t.template_id
if (t.show_url) {
if (activeTemplateId === id) {
setActiveTemplateId(null)
} else {
setIsPreviewReady(false)
setActiveTemplateId(id)
}
} else {
setInitialTemplateId(id)
setIsModalOpen(true)
}
}}
>
<button
type="button"
data-alt="try-this-button"
className="absolute top-4 left-1/2 -translate-x-1/2 z-20 opacity-0 group-hover:opacity-100 transition-opacity duration-300 ease-out bg-white/40 text-white hover:bg-white hover:text-black text-slate-900 text-xs font-medium px-[clamp(0.5rem,3cqw,0.75rem)] py-[clamp(0.25rem,1cqh,0.5rem)] rounded-full border-white/60 shadow-sm"
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
const id = t.id || t.template_id
dispatch(selectTemplateById(id))
}}
>
Try this
</button>
</div>
</div>
)
})}
</div>
)}
{/* Centered modal for active template preview */}
{activeTemplateId && (() => {
const active = topTemplates.find((x) => (x.id || x.template_id) === activeTemplateId)
if (!active || !active.show_url) return null
return (
<TemplatePreviewModal
open={!!activeTemplateId}
videoUrl={active.show_url}
onClose={() => setActiveTemplateId(null)}
title={active.name}
description={active.generateText || active.name}
onPrimaryAction={() => {
const id = active.id || active.template_id
dispatch(selectTemplateById(id))
setActiveTemplateId(null)
}}
primaryLabel="Try this"
/>
)
})()}
<PcTemplateModal
isTemplateCreating={isTemplateCreating}
setIsTemplateCreating={setIsTemplateCreating}
isRoleGenerating={isRoleGenerating}
setIsRoleGenerating={setIsRoleGenerating}
isItemGenerating={isItemGenerating}
setIsItemGenerating={setIsItemGenerating}
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
initialTemplateId={initialTemplateId}
configOptions={{ mode: "auto", resolution: "720p", language: "english", videoDuration: "auto" }}
/>
</section>
)
}
export default FamousTemplate