| 1 | "use client"; |
| 2 | |
| 3 | import { DRAG_ITEM_BLOCK } from "@platejs/dnd"; |
| 4 | import { GripVertical } from "lucide-react"; |
| 5 | import { useMemo, useState } from "react"; |
| 6 | import { useDrag } from "react-dnd"; |
| 7 | import { toast } from "sonner"; |
| 8 | |
| 9 | import { |
| 10 | createMediaEmbedNode, |
| 11 | mediaEmbedItems, |
| 12 | type MediaEmbedItem, |
| 13 | } from "@/components/plate/ui/media-embeds"; |
| 14 | import { useDebouncedSave } from "@/hooks/presentation/useDebouncedSave"; |
| 15 | import { cn } from "@/lib/utils"; |
| 16 | import { usePresentationState } from "@/states/presentation-state"; |
| 17 | import { PanelSearchFilter } from "./PanelSearchFilter"; |
| 18 | import { matchesPanelSearch } from "./PanelSearchFilter"; |
| 19 | |
| 20 | function getMediaFilterValue(item: MediaEmbedItem): string { |
| 21 | if (["youtube", "vimeo", "loom"].includes(item.embedType)) return "video"; |
| 22 | if (["twitter"].includes(item.embedType)) return "social"; |
| 23 | if (["figma", "codepen"].includes(item.embedType)) return "design"; |
| 24 | if (["image", "infographic"].includes(item.embedType)) return "media"; |
| 25 | return "web"; |
| 26 | } |
| 27 | |
| 28 | export function MediaEmbedPanel() { |
| 29 | const [searchQuery, setSearchQuery] = useState(""); |
| 30 | const filteredItems = useMemo( |
| 31 | () => |
| 32 | mediaEmbedItems.filter((item) => { |
| 33 | const category = getMediaFilterValue(item); |
| 34 | |
| 35 | return matchesPanelSearch(searchQuery, [ |
| 36 | item.label, |
| 37 | item.description, |
| 38 | item.embedType, |
| 39 | category, |
| 40 | ]); |
| 41 | }), |
| 42 | [searchQuery], |
| 43 | ); |
| 44 | |
| 45 | return ( |
| 46 | <div className="flex h-full flex-col overflow-hidden"> |
| 47 | <PanelSearchFilter |
| 48 | onQueryChange={setSearchQuery} |
| 49 | placeholder="Search embeds..." |
| 50 | query={searchQuery} |
| 51 | /> |
| 52 | <div className="scrollbar-thin flex-1 overflow-y-auto px-4 py-4 scrollbar-thumb-primary scrollbar-track-transparent"> |
| 53 | {filteredItems.length > 0 ? ( |
| 54 | <div className="grid grid-cols-2 gap-4"> |
| 55 | {filteredItems.map((item) => ( |
| 56 | <MediaEmbedCard key={item.key} item={item} /> |
| 57 | ))} |
| 58 | </div> |
| 59 | ) : ( |
| 60 | <div className="flex h-full items-center justify-center px-6 text-center text-sm text-muted-foreground"> |
| 61 | No embeds match your search. |
| 62 | </div> |
| 63 | )} |
| 64 | </div> |
| 65 | </div> |
| 66 | ); |
| 67 | } |
| 68 | |
| 69 | function MediaEmbedCard({ item }: { item: MediaEmbedItem }) { |
| 70 | const currentSlideId = usePresentationState((s) => s.currentSlideId); |
| 71 | const slides = usePresentationState((s) => s.slides); |
| 72 | const setSlides = usePresentationState((s) => s.setSlides); |
| 73 | const openInfographicGenerationEditor = usePresentationState( |
| 74 | (s) => s.openInfographicGenerationEditor, |
| 75 | ); |
| 76 | const { saveImmediately } = useDebouncedSave(); |
| 77 | |
| 78 | const [{ isDragging }, drag] = useDrag(() => ({ |
| 79 | type: DRAG_ITEM_BLOCK, |
| 80 | item: { |
| 81 | id: `external-media-${item.key}`, |
| 82 | element: createMediaEmbedNode(item.embedType), |
| 83 | }, |
| 84 | collect: (monitor) => ({ isDragging: monitor.isDragging() }), |
| 85 | })); |
| 86 | |
| 87 | const selectEmbedType = () => { |
| 88 | const currentSlide = slides.find((s) => s.id === currentSlideId); |
| 89 | |
| 90 | if (currentSlide) { |
| 91 | setSlides( |
| 92 | slides.map((slide) => |
| 93 | slide.id === currentSlideId |
| 94 | ? { |
| 95 | ...slide, |
| 96 | rootImage: { |
| 97 | ...(slide.rootImage ?? { query: "" }), |
| 98 | embedType: item.embedType, |
| 99 | url: "", // Clear URL so user can enter a new one |
| 100 | chartType: undefined, // Clear chart type since we're switching to embed |
| 101 | chartData: undefined, |
| 102 | chartOptions: undefined, |
| 103 | }, |
| 104 | } |
| 105 | : slide, |
| 106 | ), |
| 107 | ); |
| 108 | void saveImmediately(); |
| 109 | toast.success(`Changed to ${item.label} embed`); |
| 110 | if (item.embedType === "infographic") { |
| 111 | openInfographicGenerationEditor(); |
| 112 | } |
| 113 | } else { |
| 114 | toast.error("Please select a root image first"); |
| 115 | } |
| 116 | }; |
| 117 | |
| 118 | return ( |
| 119 | <div |
| 120 | ref={(el) => { |
| 121 | if (el) drag(el); |
| 122 | }} |
| 123 | onClick={selectEmbedType} |
| 124 | className={cn( |
| 125 | "group relative flex cursor-pointer flex-col items-center justify-center space-y-3 rounded-xl border border-border/50 p-5 transition-all duration-300 hover:shadow-lg", |
| 126 | "bg-card hover:bg-secondary/50", |
| 127 | "hover:-translate-y-1 hover:scale-105 hover:border-border", |
| 128 | isDragging && "scale-95 opacity-50", |
| 129 | )} |
| 130 | > |
| 131 | <div className="absolute top-2 left-2"> |
| 132 | <GripVertical className="size-4 cursor-grab text-muted-foreground transition-colors group-hover:text-foreground active:cursor-grabbing" /> |
| 133 | </div> |
| 134 | <div |
| 135 | className={cn( |
| 136 | "flex size-14 items-center justify-center rounded-lg text-3xl shadow transition-all duration-300 group-hover:scale-110", |
| 137 | "bg-muted", |
| 138 | )} |
| 139 | > |
| 140 | {item.icon} |
| 141 | </div> |
| 142 | <div className="text-center"> |
| 143 | <div className="text-sm font-bold text-foreground">{item.label}</div> |
| 144 | <div className="mt-1 text-xs text-muted-foreground"> |
| 145 | {item.description} |
| 146 | </div> |
| 147 | </div> |
| 148 | </div> |
| 149 | ); |
| 150 | } |
| 151 |