forked from 77media/video-flow
207 lines
8.7 KiB
TypeScript
207 lines
8.7 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={`mb-4 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-1 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"
|
|
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-4 py-2 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
|