Files
flatrender/src/components/sections/TemplateGallery.tsx
T

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>
);
}