import { ArrowLeft, Download, ImagePlus, Laugh, Loader2, RotateCcw, Search, Sparkles, } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { cn } from "@/lib/utils"; import { CATEGORIES, FONT_FAMILY_MAP, injectMemeFonts, PRESET_LAYOUTS, type TemplateTextBox, type TextBoxValue, type TextLayout, useMemeStore, } from "@/stores/meme-store"; const INPUT_CLASS = "w-full px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground"; // ── Text Preview Overlay ──────────────────────────────────────────── function TextPreviewOverlay({ boxes, textValues, fontFamily, fontSize, textColor, strokeColor, textAlign, allCaps, containerWidth, }: { boxes: TemplateTextBox[]; textValues: TextBoxValue[]; fontFamily: string; fontSize: number; textColor: string; strokeColor: string; textAlign: string; allCaps: boolean; containerWidth: number; }) { const cssFontFamily = FONT_FAMILY_MAP[fontFamily] ?? FONT_FAMILY_MAP.anton; return ( <> {boxes.map((box) => { const value = textValues.find((v) => v.id === box.id); const text = value?.text || box.defaultText || ""; const displayText = allCaps ? text.toUpperCase() : text; const boxPxW = (box.width / 100) * containerWidth; const boxPxH = (box.height / 100) * containerWidth; const autoSize = Math.max(10, Math.min(Math.floor(boxPxW / 8), Math.floor(boxPxH / 2), 48)); const appliedSize = fontSize > 0 ? fontSize : autoSize; const stroke = Math.max(1, Math.round(appliedSize * 0.04)); return (
{displayText}
); })} ); } // ── Gallery Phase ─────────────────────────────────────────────────── function TemplateGallery() { const templates = useMemeStore((s) => s.templates); const searchQuery = useMemeStore((s) => s.searchQuery); const activeCategory = useMemeStore((s) => s.activeCategory); const selectTemplate = useMemeStore((s) => s.selectTemplate); const setSearchQuery = useMemeStore((s) => s.setSearchQuery); const setActiveCategory = useMemeStore((s) => s.setActiveCategory); const setCustomImage = useMemeStore((s) => s.setCustomImage); const fileInputRef = useRef(null); const filtered = useMemo(() => { let result = templates; if (activeCategory !== "all") { result = result.filter((t) => t.category === activeCategory); } if (searchQuery.trim()) { const q = searchQuery.toLowerCase().trim(); result = result.filter( (t) => t.name.toLowerCase().includes(q) || t.aliases.some((a) => a.toLowerCase().includes(q)) || t.tags.some((tag) => tag.toLowerCase().includes(q)), ); } return result; }, [templates, searchQuery, activeCategory]); const categoryCounts = useMemo(() => { const counts: Record = { all: templates.length }; for (const t of templates) { counts[t.category] = (counts[t.category] ?? 0) + 1; } return counts; }, [templates]); const handleUploadCustom = useCallback(() => { fileInputRef.current?.click(); }, []); const handleFileChange = useCallback( (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; setCustomImage(file); e.target.value = ""; }, [setCustomImage], ); return (
{/* Search */}
setSearchQuery(e.target.value)} placeholder="Search templates..." className={cn(INPUT_CLASS, "pl-8")} />
{/* Categories */}
{CATEGORIES.map((cat) => ( ))}
{/* Upload custom */} {/* Template grid */}
{filtered.map((t) => ( ))}
{filtered.length === 0 && (
No templates match your search
)}
); } // ── Layout Picker Phase (custom image) ────────────────────────────── function LayoutPicker() { const customImageUrl = useMemeStore((s) => s.customImageUrl); const customLayout = useMemeStore((s) => s.customLayout); const setCustomLayout = useMemeStore((s) => s.setCustomLayout); const backToGallery = useMemeStore((s) => s.backToGallery); const selected = customLayout ?? "top-bottom"; if (!customImageUrl) return null; return (

Choose a text layout

{( Object.entries(PRESET_LAYOUTS) as [TextLayout, (typeof PRESET_LAYOUTS)[TextLayout]][] ).map(([key, layout]) => ( ))}
); } // ── Editor Phase ──────────────────────────────────────────────────── function EditorPreview() { const selectedTemplate = useMemeStore((s) => s.selectedTemplate); const customImageUrl = useMemeStore((s) => s.customImageUrl); const customLayout = useMemeStore((s) => s.customLayout); const textBoxValues = useMemeStore((s) => s.textBoxValues); const fontFamily = useMemeStore((s) => s.fontFamily); const fontSize = useMemeStore((s) => s.fontSize); const textColor = useMemeStore((s) => s.textColor); const strokeColor = useMemeStore((s) => s.strokeColor); const textAlign = useMemeStore((s) => s.textAlign); const allCaps = useMemeStore((s) => s.allCaps); const containerRef = useRef(null); const [containerWidth, setContainerWidth] = useState(600); useEffect(() => { const el = containerRef.current; if (!el) return; const ro = new ResizeObserver((entries) => { for (const entry of entries) { setContainerWidth(entry.contentRect.width); } }); ro.observe(el); return () => ro.disconnect(); }, []); const imageSrc = selectedTemplate ? `/api/v1/meme-templates/full/${selectedTemplate.filename}` : (customImageUrl ?? ""); const textBoxes = selectedTemplate ? selectedTemplate.textBoxes : ((customLayout && PRESET_LAYOUTS[customLayout]?.boxes) ?? PRESET_LAYOUTS["top-bottom"].boxes); return (
Template preview
); } // ── Result Phase ──────────────────────────────────────────────────── function ResultView() { const resultUrl = useMemeStore((s) => s.resultUrl); const backToEditor = useMemeStore((s) => s.backToEditor); const backToGallery = useMemeStore((s) => s.backToGallery); if (!resultUrl) return null; return (
Generated meme
Download
); } // ── Loading / Error States ────────────────────────────────────────── function LoadingView() { return (
Loading templates...
); } function ErrorView() { const error = useMemeStore((s) => s.error); return (

{error}

); } // ── Main Preview Component (ResultsPanel) ─────────────────────────── export function MemeGeneratorPreview() { const phase = useMemeStore((s) => s.phase); const loading = useMemeStore((s) => s.loading); const error = useMemeStore((s) => s.error); const templates = useMemeStore((s) => s.templates); const fetchTemplates = useMemeStore((s) => s.fetchTemplates); // Inject fonts on mount useEffect(() => { injectMemeFonts(); }, []); // Fetch templates on mount useEffect(() => { if (templates.length === 0) { fetchTemplates(); } }, [templates.length, fetchTemplates]); if (loading && templates.length === 0) { return ; } if (error && phase === "gallery") { return ; } if (phase === "gallery") { return ; } if (phase === "layout-picker") { return ; } if (phase === "editor") { return ; } if (phase === "result") { return ; } return null; }