"use client"; import Image from "next/image"; import type { CSSProperties } from "react"; import { useEffect, useState } from "react"; import { toast } from "sonner"; import { DraggableItem } from "@/components/editor/panels/assets/draggable-item"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Spinner } from "@/components/ui/spinner"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useEditor } from "@/editor/use-editor"; import { resolveStickerIntrinsicSize } from "@/stickers"; import { buildGraphicElement, buildStickerElement, } from "@/timeline/element-utils"; import { STICKER_CATEGORIES } from "@/stickers/categories"; import { getRegionLabel, resolveQueryToRegions } from "@/stickers"; import { parseShapeStickerId } from "@/stickers/providers/shapes"; import type { TimelineDragData } from "@/timeline/drag"; import type { StickerBrowseSection, StickerCategory, StickerItem as StickerData, } from "@/stickers"; import { useStickersStore } from "@/stickers/stickers-store"; import { cn } from "@/utils/ui"; import { HappyIcon, } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; export function StickersView() { const { browseContent, browseStickers, searchQuery, searchStickers, selectedCategory, setSearchQuery, setSelectedCategory, viewMode, } = useStickersStore(); useEffect(() => { if (viewMode === "browse" && !browseContent) { void browseStickers(); } }, [browseContent, browseStickers, viewMode]); return (
{ setSearchQuery({ query: e.target.value }); void searchStickers({ query: e.target.value }); }} showClearIcon onClear={() => { setSearchQuery({ query: "" }); void searchStickers({ query: "" }); }} className="w-full" containerClassName="w-full" />
{ setSelectedCategory({ category: value as StickerCategory }); }} variant="underline" className="mt-2 flex min-h-0 flex-1 flex-col" > {Object.entries(STICKER_CATEGORIES).map(([key, label]) => ( {label} ))}
); } function StickerGrid({ items, shouldCapSize = false, }: { items: StickerData[]; shouldCapSize?: boolean; }) { const gridStyle: CSSProperties & { "--sticker-min": string; "--sticker-max"?: string; } = { gridTemplateColumns: shouldCapSize ? "repeat(auto-fill, minmax(var(--sticker-min, 80px), var(--sticker-max, 140px)))" : "repeat(auto-fill, minmax(var(--sticker-min, 80px), 1fr))", "--sticker-min": "80px", ...(shouldCapSize ? { "--sticker-max": "140px" } : {}), }; return (
{items.map((item) => ( ))}
); } function StickerRow({ items }: { items: StickerData[] }) { return (
{items.map((item) => (
))}
); } function EmptyView({ message }: { message: string }) { return (

No stickers found

{message}

); } function RegionBanner({ region }: { region: string }) { return (
{region}
); } function StickersContentView() { const { browseContent, clearRecentStickers, isBrowsing, isSearching, searchQuery, searchResults, selectedCategory, setSelectedCategory, viewMode, } = useStickersStore(); if (viewMode === "search") { if (isSearching) { return (
); } if (searchResults?.items.length) { const normalizedQuery = searchQuery.trim().toLowerCase(); const isRegionSearch = selectedCategory === "flags" && resolveQueryToRegions({ query: normalizedQuery }) !== null; const regionLabel = getRegionLabel({ query: normalizedQuery }); return (
{isRegionSearch && }
{searchResults.total} results
); } // "all" tab search — sections are in browseContent, fall through to section rendering below if (selectedCategory !== "all" && searchQuery) { return ; } } if (isBrowsing && !browseContent) { return (
); } if (!browseContent?.sections.length) { const categoryLabel = STICKER_CATEGORIES[selectedCategory]; return ( ); } return (
{browseContent.sections.map((section) => ( { setSelectedCategory({ category }); }} /> ))}
); } function StickerSection({ section, onClearRecent, onSeeAll, }: { section: StickerBrowseSection; onClearRecent: () => void; onSeeAll: (category: StickerCategory) => void; }) { const hasHeader = Boolean(section.title) || section.id === "recent" || section.action; return (
{hasHeader && (
{section.title ? (

{section.title}

) : (
)}
{section.id === "recent" && ( )} {section.action?.type === "see-all" && section.action.category && ( )}
)} {section.layout === "row" ? ( ) : ( )}
); } interface StickerItemProps { item: StickerData; shouldCapSize?: boolean; containerClassName?: string; } function StickerItem({ item, shouldCapSize = false, containerClassName, }: StickerItemProps) { const editor = useEditor(); const { addToRecentStickers } = useStickersStore(); const [isAdding, setIsAdding] = useState(false); const [hasImageError, setHasImageError] = useState(false); useEffect(() => { if (!item.id) { return; } setHasImageError(false); }, [item.id]); const displayName = item.name; const shapePreset = item.provider === "shapes" ? parseShapeStickerId({ stickerId: item.id }) : null; const handleAdd = async () => { setIsAdding(true); try { const currentTime = editor.playback.getCurrentTime(); let element: | ReturnType | ReturnType; if (shapePreset) { element = buildGraphicElement({ definitionId: shapePreset.definitionId, name: shapePreset.name, startTime: currentTime, params: shapePreset.params, }); } else { const { width: intrinsicWidth, height: intrinsicHeight } = await resolveStickerIntrinsicSize({ stickerId: item.id }); element = buildStickerElement({ stickerId: item.id, name: item.name, startTime: currentTime, intrinsicWidth, intrinsicHeight, }); } editor.timeline.insertElement({ placement: { mode: "auto" }, element, }); addToRecentStickers({ stickerId: item.id }); } catch (error) { console.error("Failed to add sticker:", error); toast.error("Failed to add sticker to timeline"); } finally { setIsAdding(false); } }; const preview = (
{hasImageError ? ( {displayName} ) : ( {displayName} setHasImageError(true)} loading="lazy" unoptimized /> )}
); const dragData: TimelineDragData = shapePreset ? { id: item.id, type: "graphic", name: displayName, definitionId: shapePreset.definitionId, params: shapePreset.params ?? {}, } : { id: item.id, type: "sticker", name: displayName, stickerId: item.id, }; return (
{isAdding && (
)}
); }