"use client";
import { useEffect, useState, useMemo } from "react";
import { useStickersStore } from "@/stores/stickers-store";
import { useMediaStore } from "@/stores/media-store";
import { useProjectStore } from "@/stores/project-store";
import { useTimelineStore } from "@/stores/timeline-store";
import { usePlaybackStore } from "@/stores/playback-store";
import {
Loader2,
Grid3X3,
Hash,
Smile,
Clock,
X,
Sparkles,
ArrowRight,
StickerIcon,
} from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import { Separator } from "@/components/ui/separator";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
getIconSvgUrl,
buildIconSvgUrl,
ICONIFY_HOSTS,
POPULAR_COLLECTIONS,
} from "@/lib/iconify-api";
import { cn, generateUUID } from "@/lib/utils";
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
import Image from "next/image";
import { DraggableMediaItem } from "@/components/ui/draggable-item";
import { InputWithBack } from "@/components/ui/input-with-back";
import { StickerCategory } from "@/stores/stickers-store";
import { useInfiniteScroll } from "@/hooks/use-infinite-scroll";
export function StickersView() {
const { selectedCategory, setSelectedCategory } = useStickersStore();
return (
{
if (["all", "general", "brands", "emoji"].includes(v)) {
setSelectedCategory(v as StickerCategory);
}
}}
className="flex flex-col h-full"
>
All
Icons
Brands
Emoji
);
}
function StickerGrid({
icons,
onAdd,
addingSticker,
}: {
icons: string[];
onAdd: (iconName: string) => void;
addingSticker: string | null;
}) {
return (
{icons.map((iconName) => (
))}
);
}
function CollectionGrid({
collections,
onSelectCollection,
}: {
collections: Array<{
prefix: string;
name: string;
total: number;
category?: string;
}>;
onSelectCollection: (prefix: string) => void;
}) {
return (
{collections.map((collection) => (
onSelectCollection(collection.prefix)}
/>
))}
);
}
function EmptyView({ message }: { message: string }) {
return (
No stickers found
{message}
);
}
function StickersContentView({ category }: { category: StickerCategory }) {
const { activeProject } = useProjectStore();
const { addMediaAtTime } = useTimelineStore();
const { currentTime } = usePlaybackStore();
const { addMediaItem } = useMediaStore();
const {
searchQuery,
selectedCollection,
viewMode,
collections,
currentCollection,
searchResults,
recentStickers,
isLoadingCollections,
isLoadingCollection,
isSearching,
setSearchQuery,
setSelectedCollection,
loadCollections,
searchStickers,
downloadSticker,
clearRecentStickers,
} = useStickersStore();
const [addingSticker, setAddingSticker] = useState(null);
const [localSearchQuery, setLocalSearchQuery] = useState(searchQuery);
const [collectionsToShow, setCollectionsToShow] = useState(20);
const [showCollectionItems, setShowCollectionItems] = useState(false);
const filteredCollections = useMemo(() => {
if (category === "all") {
return Object.entries(collections).map(([prefix, collection]) => ({
prefix,
name: collection.name,
total: collection.total,
category: collection.category,
}));
}
const collectionList =
POPULAR_COLLECTIONS[category as keyof typeof POPULAR_COLLECTIONS];
if (!collectionList) return [];
return collectionList
.map((c) => {
const collection = collections[c.prefix];
return collection
? {
prefix: c.prefix,
name: c.name,
total: collection.total,
}
: null;
})
.filter(Boolean) as Array<{
prefix: string;
name: string;
total: number;
}>;
}, [collections, category]);
const { scrollAreaRef, handleScroll } = useInfiniteScroll({
onLoadMore: () => setCollectionsToShow((prev) => prev + 20),
hasMore: filteredCollections.length > collectionsToShow,
isLoading: isLoadingCollections,
enabled: viewMode === "browse" && !selectedCollection && category === "all",
});
useEffect(() => {
if (Object.keys(collections).length === 0) {
loadCollections();
}
}, []);
useEffect(() => {
const timer = setTimeout(() => {
if (localSearchQuery !== searchQuery) {
setSearchQuery(localSearchQuery);
if (localSearchQuery.trim()) {
searchStickers(localSearchQuery);
}
}
}, 500);
return () => clearTimeout(timer);
}, [localSearchQuery]);
const handleAddSticker = async (iconName: string) => {
if (!activeProject) {
toast.error("No active project");
return;
}
setAddingSticker(iconName);
try {
const file = await downloadSticker(iconName);
if (!file) {
throw new Error("Failed to download sticker");
}
const mediaItem = {
name: iconName.replace(":", "-"),
type: "image" as const,
file,
url: URL.createObjectURL(file),
width: 200,
height: 200,
duration: TIMELINE_CONSTANTS.DEFAULT_IMAGE_DURATION,
ephemeral: false,
};
await addMediaItem(activeProject.id, mediaItem);
const added = useMediaStore
.getState()
.mediaItems.find(
(m) => m.url === mediaItem.url && m.name === mediaItem.name
);
if (!added) throw new Error("Sticker not in media store");
addMediaAtTime(added, currentTime);
toast.success(`Added "${iconName}" to timeline`);
} catch (error) {
console.error("Failed to add sticker:", error);
toast.error("Failed to add sticker to timeline");
} finally {
setAddingSticker(null);
}
};
const iconsToDisplay = useMemo(() => {
if (viewMode === "search" && searchResults) {
return searchResults.icons;
}
if (viewMode === "collection" && currentCollection) {
const icons: string[] = [];
if (currentCollection.uncategorized) {
icons.push(
...currentCollection.uncategorized.map(
(name) => `${currentCollection.prefix}:${name}`
)
);
}
if (currentCollection.categories) {
Object.values(currentCollection.categories).forEach((categoryIcons) => {
icons.push(
...categoryIcons.map(
(name) => `${currentCollection.prefix}:${name}`
)
);
});
}
return icons.slice(0, 100);
}
return [];
}, [viewMode, searchResults, currentCollection]);
const isInCollection = viewMode === "collection" && !!selectedCollection;
useEffect(() => {
if (isInCollection) {
setShowCollectionItems(false);
const timer = setTimeout(() => setShowCollectionItems(true), 350);
return () => clearTimeout(timer);
} else {
setShowCollectionItems(false);
}
}, [isInCollection]);
return (
{
if (!expanded && isInCollection) {
setSelectedCollection(null);
}
}}
placeholder="Search icons..."
value={localSearchQuery}
onChange={setLocalSearchQuery}
/>
{recentStickers.length > 0 && viewMode === "browse" && (
Recent
Clear recent stickers
)}
{viewMode === "collection" && selectedCollection && (
{isLoadingCollection ? (
) : showCollectionItems ? (
) : (
)}
)}
{viewMode === "search" && (
{isSearching ? (
) : searchResults?.icons.length ? (
<>
{searchResults.total} results
>
) : searchQuery ? (
) : null}
)}
{viewMode === "browse" && !selectedCollection && (
{isLoadingCollections ? (
) : (
<>
{category !== "all" && (
Popular{" "}
{category === "general"
? "Icon Sets"
: category === "brands"
? "Brand Icons"
: "Emoji Sets"}
)}
{category === "all" && filteredCollections.length > 0 && (
)}
>
)}
)}
);
}
interface CollectionItemProps {
title: string;
subtitle: string;
onClick: () => void;
}
function CollectionItem({ title, subtitle, onClick }: CollectionItemProps) {
return (
);
}
interface StickerItemProps {
iconName: string;
onAdd: (iconName: string) => void;
isAdding?: boolean;
}
function StickerItem({ iconName, onAdd, isAdding }: StickerItemProps) {
const [imageError, setImageError] = useState(false);
const [hostIndex, setHostIndex] = useState(0);
useEffect(() => {
setImageError(false);
setHostIndex(0);
}, [iconName]);
const displayName = iconName.split(":")[1] || iconName;
const collectionPrefix = iconName.split(":")[0];
const preview = imageError ? (
{displayName}
) : (
{
const next = hostIndex + 1;
if (next < ICONIFY_HOSTS.length) {
setHostIndex(next);
} else {
setImageError(true);
}
}}
loading="lazy"
unoptimized
/>
);
return (
onAdd(iconName)}
aspectRatio={1}
showLabel={false}
rounded={true}
variant="card"
className=""
isDraggable={false}
/>
{isAdding && (
)}
{displayName}
{collectionPrefix}
);
}