diff options
Diffstat (limited to 'apps/web/src/components/editor/media-panel/views/stickers.tsx')
| -rw-r--r-- | apps/web/src/components/editor/media-panel/views/stickers.tsx | 612 |
1 files changed, 0 insertions, 612 deletions
diff --git a/apps/web/src/components/editor/media-panel/views/stickers.tsx b/apps/web/src/components/editor/media-panel/views/stickers.tsx deleted file mode 100644 index f67450cc..00000000 --- a/apps/web/src/components/editor/media-panel/views/stickers.tsx +++ /dev/null @@ -1,612 +0,0 @@ -"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 { PanelBaseView as BaseView } from "@/components/editor/panel-base-view"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; - -import { - getIconSvgUrl, - buildIconSvgUrl, - ICONIFY_HOSTS, - POPULAR_COLLECTIONS, -} from "@/lib/iconify-api"; -import { cn } from "@/lib/utils"; -import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; -import Image from "next/image"; -import type { MediaFile } from "@/types/media"; -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 ( - <BaseView - value={selectedCategory} - onValueChange={(v) => { - if (["all", "general", "brands", "emoji"].includes(v)) { - setSelectedCategory(v as StickerCategory); - } - }} - tabs={[ - { - value: "all", - label: "All", - icon: <Grid3X3 className="h-3 w-3" />, - content: <StickersContentView category="all" />, - }, - { - value: "general", - label: "Icons", - icon: <Sparkles className="h-3 w-3" />, - content: <StickersContentView category="general" />, - }, - { - value: "brands", - label: "Brands", - icon: <Hash className="h-3 w-3" />, - content: <StickersContentView category="brands" />, - }, - { - value: "emoji", - label: "Emoji", - icon: <Smile className="h-3 w-3" />, - content: <StickersContentView category="emoji" />, - }, - ]} - className="flex flex-col h-full p-0 overflow-hidden" - /> - ); -} - -function StickerGrid({ - icons, - onAdd, - addingSticker, - capSize = false, -}: { - icons: string[]; - onAdd: (iconName: string) => void; - addingSticker: string | null; - capSize?: boolean; -}) { - return ( - <div - className="grid gap-2" - style={{ - gridTemplateColumns: capSize - ? "repeat(auto-fill, minmax(var(--sticker-min, 96px), var(--sticker-max, 160px)))" - : "repeat(auto-fit, minmax(var(--sticker-min, 96px), 1fr))", - ["--sticker-min" as any]: "96px", - ...(capSize ? ({ ["--sticker-max"]: "160px" } as any) : {}), - }} - > - {icons.map((iconName) => ( - <StickerItem - key={iconName} - iconName={iconName} - onAdd={onAdd} - isAdding={addingSticker === iconName} - capSize={capSize} - /> - ))} - </div> - ); -} - -function CollectionGrid({ - collections, - onSelectCollection, -}: { - collections: Array<{ - prefix: string; - name: string; - total: number; - category?: string; - }>; - onSelectCollection: (prefix: string) => void; -}) { - return ( - <div className="grid grid-cols-1 gap-2"> - {collections.map((collection) => ( - <CollectionItem - key={collection.prefix} - title={collection.name} - subtitle={`${collection.total.toLocaleString()} icons${collection.category ? ` • ${collection.category}` : ""}`} - onClick={() => onSelectCollection(collection.prefix)} - /> - ))} - </div> - ); -} - -function EmptyView({ message }: { message: string }) { - return ( - <div className="bg-panel h-full p-4 flex flex-col items-center justify-center gap-3"> - <StickerIcon - className="w-10 h-10 text-muted-foreground" - strokeWidth={1.5} - /> - <div className="flex flex-col gap-2 text-center"> - <p className="text-lg font-medium">No stickers found</p> - <p className="text-sm text-muted-foreground text-balance">{message}</p> - </div> - </div> - ); -} - -function StickersContentView({ category }: { category: StickerCategory }) { - const { activeProject } = useProjectStore(); - const { addElementAtTime } = useTimelineStore(); - const { currentTime } = usePlaybackStore(); - const { addMediaFile } = useMediaStore(); - const { - searchQuery, - selectedCollection, - viewMode, - collections, - currentCollection, - searchResults, - recentStickers, - isLoadingCollections, - isLoadingCollection, - isSearching, - setSearchQuery, - setSelectedCollection, - loadCollections, - searchStickers, - addStickerToTimeline, - clearRecentStickers, - setSelectedCategory, - addingSticker, - } = useStickersStore(); - - 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) => { - try { - await addStickerToTimeline(iconName); - } catch (error) { - console.error("Failed to add sticker:", error); - toast.error("Failed to add sticker to timeline"); - } - }; - - 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 ( - <div className="flex flex-col gap-5 mt-1 h-full p-4"> - <div className="space-y-3"> - <InputWithBack - isExpanded={isInCollection} - setIsExpanded={(expanded) => { - if (!expanded && isInCollection) { - setSelectedCollection(null); - } - }} - placeholder={ - category === "all" - ? "Search all stickers" - : category === "general" - ? "Search icons" - : category === "brands" - ? "Search brands" - : "Search Emojis" - } - value={localSearchQuery} - onChange={setLocalSearchQuery} - disableAnimation={true} - /> - </div> - - <div className="relative h-full overflow-hidden"> - <ScrollArea - className="flex-1 h-full" - ref={scrollAreaRef} - onScrollCapture={handleScroll} - > - <div className="flex flex-col gap-4 h-full"> - {recentStickers.length > 0 && viewMode === "browse" && ( - <div className="h-full"> - <div className="flex items-center gap-2 mb-2"> - <Clock className="h-4 w-4 text-muted-foreground" /> - <span className="text-sm font-medium">Recent</span> - <TooltipProvider> - <Tooltip> - <TooltipTrigger asChild> - <button - onClick={clearRecentStickers} - className="ml-auto h-5 w-5 p-0 rounded hover:bg-accent flex items-center justify-center" - > - <X className="h-3 w-3 text-muted-foreground" /> - </button> - </TooltipTrigger> - <TooltipContent> - <p>Clear recent stickers</p> - </TooltipContent> - </Tooltip> - </TooltipProvider> - </div> - <StickerGrid - icons={recentStickers.slice(0, 12)} - onAdd={handleAddSticker} - addingSticker={addingSticker} - capSize - /> - </div> - )} - - {viewMode === "collection" && selectedCollection && ( - <div className="h-full"> - {isLoadingCollection ? ( - <div className="flex items-center justify-center py-8"> - <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /> - </div> - ) : showCollectionItems ? ( - <StickerGrid - icons={iconsToDisplay} - onAdd={handleAddSticker} - addingSticker={addingSticker} - /> - ) : ( - <div className="flex items-center justify-center py-8"> - <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /> - </div> - )} - </div> - )} - - {viewMode === "search" && ( - <div className="h-full"> - {isSearching ? ( - <div className="flex items-center justify-center py-8"> - <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /> - </div> - ) : searchResults?.icons.length ? ( - <> - <div className="flex items-center justify-between mb-3"> - <span className="text-sm text-muted-foreground"> - {searchResults.total} results - </span> - </div> - <StickerGrid - icons={iconsToDisplay} - onAdd={handleAddSticker} - addingSticker={addingSticker} - capSize - /> - </> - ) : searchQuery ? ( - <div className="flex flex-col items-center justify-center py-8 gap-3"> - <EmptyView - message={`No stickers found for "${searchQuery}"`} - /> - {category !== "all" && ( - <Button - variant="outline" - onClick={() => { - const q = localSearchQuery || searchQuery; - if (q) { - setSearchQuery(q); - } - setSelectedCategory("all"); - if (q) { - searchStickers(q); - } - }} - > - Search in all icons - </Button> - )} - </div> - ) : null} - </div> - )} - - {viewMode === "browse" && !selectedCollection && ( - <div className="space-y-4 h-full"> - {isLoadingCollections ? ( - <div className="flex items-center justify-center py-8"> - <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /> - </div> - ) : ( - <> - {category !== "all" && ( - <div className="h-full"> - <h3 className="text-sm font-medium mb-2"> - Popular{" "} - {category === "general" - ? "Icon Sets" - : category === "brands" - ? "Brand Icons" - : "Emoji Sets"} - </h3> - <CollectionGrid - collections={filteredCollections} - onSelectCollection={setSelectedCollection} - /> - </div> - )} - - {category === "all" && filteredCollections.length > 0 && ( - <div className="h-full"> - <CollectionGrid - collections={filteredCollections.slice( - 0, - collectionsToShow - )} - onSelectCollection={setSelectedCollection} - /> - </div> - )} - </> - )} - </div> - )} - </div> - </ScrollArea> - </div> - </div> - ); -} - -interface CollectionItemProps { - title: string; - subtitle: string; - onClick: () => void; -} - -function CollectionItem({ title, subtitle, onClick }: CollectionItemProps) { - return ( - <Button - variant="outline" - className="justify-between h-auto py-2 " - onClick={onClick} - > - <div className="text-left"> - <p className="font-medium">{title}</p> - <p className="text-xs text-muted-foreground">{subtitle}</p> - </div> - <ArrowRight className="h-4 w-4" /> - </Button> - ); -} - -interface StickerItemProps { - iconName: string; - onAdd: (iconName: string) => void; - isAdding?: boolean; - capSize?: boolean; -} - -function StickerItem({ - iconName, - onAdd, - isAdding, - capSize = false, -}: 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 ? ( - <div className="w-full h-full flex items-center justify-center p-2"> - <span className="text-xs text-muted-foreground text-center break-all"> - {displayName} - </span> - </div> - ) : ( - <div className="w-full h-full p-4 flex items-center justify-center"> - <Image - src={ - hostIndex === 0 - ? getIconSvgUrl(iconName, { width: 64, height: 64 }) - : buildIconSvgUrl( - ICONIFY_HOSTS[Math.min(hostIndex, ICONIFY_HOSTS.length - 1)], - iconName, - { width: 64, height: 64 } - ) - } - alt={displayName} - width={64} - height={64} - className="w-full h-full object-contain" - style={ - capSize - ? { - maxWidth: "var(--sticker-max, 160px)", - maxHeight: "var(--sticker-max, 160px)", - } - : undefined - } - onError={() => { - const next = hostIndex + 1; - if (next < ICONIFY_HOSTS.length) { - setHostIndex(next); - } else { - setImageError(true); - } - }} - loading="lazy" - unoptimized - /> - </div> - ); - - return ( - <Tooltip> - <TooltipTrigger asChild> - <div - className={cn( - "relative", - isAdding && "opacity-50 pointer-events-none" - )} - > - <DraggableMediaItem - name={displayName} - preview={preview} - dragData={{ - id: "sticker-placeholder", - type: "image", - name: displayName, - }} - onAddToTimeline={() => onAdd(iconName)} - aspectRatio={1} - showLabel={false} - rounded={true} - variant="card" - className="" - containerClassName="w-full" - isDraggable={false} - /> - {isAdding && ( - <div className="absolute inset-0 bg-black/60 flex items-center justify-center rounded-md z-10"> - <Loader2 className="h-6 w-6 animate-spin text-white" /> - </div> - )} - </div> - </TooltipTrigger> - <TooltipContent> - <div className="space-y-1"> - <p className="font-medium">{displayName}</p> - <p className="text-xs text-muted-foreground">{collectionPrefix}</p> - </div> - </TooltipContent> - </Tooltip> - ); -} |
