164 lines
5.8 KiB
TypeScript
164 lines
5.8 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import Link from "next/link";
|
|
import { AnimatePresence, motion } from "framer-motion";
|
|
import { ArrowRight } from "lucide-react";
|
|
import { useTranslations } from "next-intl";
|
|
|
|
import { cn } from "@/lib/utils";
|
|
import { createVideoProject } from "@/lib/create-video-project";
|
|
|
|
import { SectionReveal } from "./SectionReveal";
|
|
import { TemplateCard } from "./TemplateCard";
|
|
import {
|
|
FILTER_TABS,
|
|
filterTemplates,
|
|
getTemplateImageSrc,
|
|
type FilterTab,
|
|
type TemplateItem,
|
|
} from "./template-gallery-data";
|
|
|
|
export interface TemplateGalleryProps {
|
|
className?: string;
|
|
}
|
|
|
|
export function TemplateGallery({ className }: TemplateGalleryProps) {
|
|
const router = useRouter();
|
|
const t = useTranslations("templates");
|
|
const [activeTab, setActiveTab] = useState<FilterTab>("All");
|
|
const [usingTemplateId, setUsingTemplateId] = useState<string | null>(null);
|
|
const filtered = filterTemplates(activeTab);
|
|
|
|
/** Map filter tab key → translated label */
|
|
const tabLabel: Record<FilterTab, string> = {
|
|
All: t("tabAll"),
|
|
Videos: t("tabVideos"),
|
|
Images: t("tabImages"),
|
|
"Social Media": t("tabSocial"),
|
|
Business: t("tabBusiness"),
|
|
};
|
|
|
|
const handleUseTemplate = useCallback(
|
|
async (template: TemplateItem) => {
|
|
if (usingTemplateId) return;
|
|
setUsingTemplateId(template.id);
|
|
|
|
// Image templates → create an image project (future)
|
|
// All others → video project
|
|
const isImage = template.category === "Images";
|
|
if (isImage) {
|
|
router.push("/dashboard");
|
|
setUsingTemplateId(null);
|
|
return;
|
|
}
|
|
|
|
const result = await createVideoProject({ name: template.name });
|
|
setUsingTemplateId(null);
|
|
|
|
if (!result.ok) {
|
|
// Dev mode: Supabase not configured → go to new-project onboarding
|
|
if (result.error.includes("Supabase is not configured")) {
|
|
router.push("/studio/video/new");
|
|
return;
|
|
}
|
|
// Any other failure (unauth, server error) → send to sign-in
|
|
router.push(`/auth?next=${encodeURIComponent("/templates")}`);
|
|
return;
|
|
}
|
|
|
|
router.push(`/studio/video/${result.projectId}`);
|
|
},
|
|
[router, usingTemplateId]
|
|
);
|
|
|
|
return (
|
|
<section
|
|
id="templates"
|
|
className={cn("scroll-mt-20 w-full bg-white py-20 sm:py-28", className)}
|
|
>
|
|
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
<SectionReveal>
|
|
<h2 className="text-center font-heading text-3xl font-bold tracking-tight text-neutral-900 sm:text-4xl">
|
|
{t("heading")}
|
|
</h2>
|
|
</SectionReveal>
|
|
|
|
{/* Filter tabs */}
|
|
<SectionReveal className="mt-10 flex flex-wrap items-center justify-center gap-2">
|
|
{FILTER_TABS.map((tab) => {
|
|
const isActive = activeTab === tab;
|
|
return (
|
|
<button
|
|
key={tab}
|
|
type="button"
|
|
onClick={() => setActiveTab(tab)}
|
|
className={cn(
|
|
"relative rounded-lg px-4 py-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-600 focus-visible:ring-offset-2",
|
|
isActive
|
|
? "text-white"
|
|
: "text-neutral-600 hover:text-neutral-900"
|
|
)}
|
|
>
|
|
{isActive && (
|
|
<motion.span
|
|
layoutId="template-gallery-tab"
|
|
className="absolute inset-0 rounded-lg bg-primary-600 shadow-sm"
|
|
transition={{ type: "spring", stiffness: 400, damping: 30 }}
|
|
/>
|
|
)}
|
|
<span className="relative z-10">{tabLabel[tab]}</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</SectionReveal>
|
|
|
|
{/* Card grid — flex + justify-center so partial rows are centred */}
|
|
<motion.div
|
|
layout
|
|
className="mt-12 flex flex-wrap justify-center gap-6"
|
|
>
|
|
<AnimatePresence mode="popLayout">
|
|
{filtered.map((template) => (
|
|
<motion.div
|
|
key={template.id}
|
|
layout
|
|
initial={{ opacity: 0, y: 16, scale: 0.96 }}
|
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
exit={{ opacity: 0, y: -8, scale: 0.96 }}
|
|
transition={{ duration: 0.4, ease: "easeOut" }}
|
|
className="w-full sm:w-[calc(50%-12px)] lg:w-[calc(25%-18px)] max-w-[320px]"
|
|
>
|
|
<TemplateCard
|
|
templateId={template.id}
|
|
name={template.name}
|
|
category={template.category}
|
|
imageSrc={getTemplateImageSrc(template.id)}
|
|
previewVideoUrl={template.previewVideoUrl}
|
|
previewSeed={template.id}
|
|
priority={filtered.indexOf(template) < 4}
|
|
onUseTemplate={() => void handleUseTemplate(template)}
|
|
isUsingTemplate={usingTemplateId === template.id}
|
|
useTemplateLabel={t("useTemplate")}
|
|
openingLabel={t("opening")}
|
|
/>
|
|
</motion.div>
|
|
))}
|
|
</AnimatePresence>
|
|
</motion.div>
|
|
|
|
<SectionReveal className="mt-12 text-center">
|
|
<Link
|
|
href="/templates"
|
|
className="inline-flex items-center gap-1 text-sm font-semibold text-primary-600 transition-colors hover:text-primary-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-600 focus-visible:ring-offset-2 rounded-sm"
|
|
>
|
|
{t("browseAll")}
|
|
<ArrowRight className="h-4 w-4" aria-hidden />
|
|
</Link>
|
|
</SectionReveal>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|