"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 (
);
}
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 (
);
}
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" && (
Clear
)}
{section.action?.type === "see-all" && section.action.category && (
{
onSeeAll(section.action?.category as StickerCategory);
}}
>
See all
)}
)}
{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}
) : (
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 (
);
}