返回 presentation-ai
media-embed-element-static.tsx
根目录 / src / components / notebook / presentation / editor / custom-elements / static / media-embed-element-static.tsx
1 import Image from "next/image";
2 import { type TMediaEmbedElement } from "platejs";
3 import { SlateElement, type SlateElementProps } from "platejs/static";
4 import type * as React from "react";
5 import LiteYouTubeEmbed from "react-lite-youtube-embed";
6 import { Tweet } from "react-tweet";
7
8 import { embedTypeConfig } from "@/components/plate/ui/media-embeds";
9 import { cn } from "@/lib/utils";
10
11 // Extended properties for media embed elements
12 interface MediaEmbedProps {
13 provider?: string;
14 url?: string;
15 id?: string;
16 alignment?: "center" | "left" | "right";
17 align?: "center" | "left" | "right";
18 width?: number | string;
19 }
20
21 function getEmbedAspectRatioClass(provider: string): string {
22 switch (provider) {
23 case "vimeo":
24 return "pb-[75%]";
25 case "coub":
26 return "pb-[51.25%]";
27 case "dailymotion":
28 return "pb-[56.0417%]";
29 default:
30 return "pb-[56.25%]";
31 }
32 }
33
34 // Static placeholder for media embed (no interactivity)
35 function MediaEmbedPlaceholderStatic({
36 embedType,
37 className,
38 }: {
39 embedType: string;
40 className?: string;
41 }) {
42 const config =
43 embedTypeConfig[embedType as keyof typeof embedTypeConfig] ||
44 embedTypeConfig.youtube;
45 const isInfographic = embedType === "infographic";
46
47 return (
48 <div
49 className={cn(
50 "relative w-full overflow-hidden rounded-lg border-2 border-dashed",
51 className,
52 )}
53 >
54 {isInfographic && (
55 <>
56 <Image
57 unoptimized
58 width={400}
59 height={300}
60 src="/placeholder.svg"
61 alt=""
62 className="absolute inset-0 h-full w-full object-cover opacity-45"
63 />
64 <div className="absolute inset-0 bg-background/5" />
65 </>
66 )}
67 <div className="relative flex min-h-50 flex-col items-center justify-center gap-y-4 p-6">
68 <div className="flex flex-col items-center gap-y-2">
69 <div className="text-center">
70 <h3 className="text-lg font-semibold text-muted-foreground">
71 {config?.name || "Media Embed"}
72 </h3>
73 <p className="text-sm text-muted-foreground">No URL provided</p>
74 </div>
75 </div>
76 </div>
77 </div>
78 );
79 }
80
81 // Static renderer for media embed element
82 export function MediaEmbedElementStatic(
83 props: SlateElementProps<TMediaEmbedElement & MediaEmbedProps>,
84 ) {
85 const element = props.element;
86
87 const align = element.alignment || element.align || "center";
88 const provider = element.provider || "";
89 const url = element.url || "";
90 const id = element.id || "";
91 const width = element.width;
92
93 // Determine embed type and if we should show placeholder
94 const shouldShowPlaceholder = !url || url.trim() === "";
95 const isTweet = provider === "twitter";
96 const isVideo = provider && ["youtube", "vimeo", "loom"].includes(provider);
97 const isYoutube = provider === "youtube";
98 const isInfographic = provider === "infographic";
99
100 const containerStyles: React.CSSProperties = {
101 display: "flex",
102 justifyContent:
103 align === "left"
104 ? "flex-start"
105 : align === "right"
106 ? "flex-end"
107 : "center",
108 };
109
110 return (
111 <SlateElement {...props} className={cn("py-2.5", props.className)}>
112 <figure
113 className="group relative m-0 w-full cursor-default"
114 contentEditable={false}
115 >
116 <div style={containerStyles}>
117 <div className="w-full max-w-full" style={{ width }}>
118 {shouldShowPlaceholder ? (
119 <MediaEmbedPlaceholderStatic
120 embedType={provider || "youtube"}
121 className="w-full"
122 />
123 ) : isVideo ? (
124 isYoutube ? (
125 <LiteYouTubeEmbed
126 id={id}
127 title="youtube"
128 wrapperClass={cn(
129 "rounded-sm",
130 "relative block cursor-pointer bg-black bg-cover bg-center contain-content",
131 "[&.lyt-activated]:before:absolute [&.lyt-activated]:before:top-0 [&.lyt-activated]:before:h-15 [&.lyt-activated]:before:w-full [&.lyt-activated]:before:bg-top [&.lyt-activated]:before:bg-repeat-x [&.lyt-activated]:before:pb-12.5 [&.lyt-activated]:before:[transition:all_0.2s_cubic-bezier(0,0,0.2,1)]",
132 "[&.lyt-activated]:before:bg-[url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAADGCAYAAAAT+OqFAAAAdklEQVQoz42QQQ7AIAgEF/T/D+kbq/RWAlnQyyazA4aoAB4FsBSA/bFjuF1EOL7VbrIrBuusmrt4ZZORfb6ehbWdnRHEIiITaEUKa5EJqUakRSaEYBJSCY2dEstQY7AuxahwXFrvZmWl2rh4JZ07z9dLtesfNj5q0FU3A5ObbwAAAABJRU5ErkJggg==)]",
133 'after:block after:pb-(--aspect-ratio) after:content-[""]',
134 "[&_>_iframe]:absolute [&_>_iframe]:top-0 [&_>_iframe]:left-0 [&_>_iframe]:size-full",
135 "[&_>_.lty-playbtn]:z-1 [&_>_.lty-playbtn]:h-11.5 [&_>_.lty-playbtn]:w-17.5 [&_>_.lty-playbtn]:rounded-[14%] [&_>_.lty-playbtn]:bg-[#212121] [&_>_.lty-playbtn]:opacity-80 [&_>_.lty-playbtn]:[transition:all_0.2s_cubic-bezier(0,0,0.2,1)]",
136 "[&:hover_>_.lty-playbtn]:bg-[red] [&:hover_>_.lty-playbtn]:opacity-100",
137 '[&_>_.lty-playbtn]:before:border-y-11 [&_>_.lty-playbtn]:before:border-r-0 [&_>_.lty-playbtn]:before:border-l-19 [&_>_.lty-playbtn]:before:border-[transparent_transparent_transparent_#fff] [&_>_.lty-playbtn]:before:content-[""]',
138 "[&_>_.lty-playbtn]:absolute [&_>_.lty-playbtn]:top-1/2 [&_>_.lty-playbtn]:left-1/2 [&_>_.lty-playbtn]:transform-[translate3d(-50%,-50%,0)]",
139 "[&_>_.lty-playbtn]:before:absolute [&_>_.lty-playbtn]:before:top-1/2 [&_>_.lty-playbtn]:before:left-1/2 [&_>_.lty-playbtn]:before:transform-[translate3d(-50%,-50%,0)]",
140 "[&.lyt-activated]:cursor-[unset]",
141 "[&.lyt-activated]:before:pointer-events-none [&.lyt-activated]:before:opacity-0",
142 "[&.lyt-activated_>_.lty-playbtn]:pointer-events-none [&.lyt-activated_>_.lty-playbtn]:opacity-0!",
143 )}
144 />
145 ) : (
146 <div
147 className={cn(
148 "relative w-full",
149 getEmbedAspectRatioClass(provider),
150 )}
151 >
152 <iframe
153 className={cn(
154 "absolute top-0 left-0 size-full rounded-sm border-0",
155 )}
156 title="embed"
157 src={url}
158 sandbox="allow-forms allow-scripts allow-popups allow-popups-to-escape-sandbox allow-presentation"
159 allowFullScreen
160 />
161 </div>
162 )
163 ) : isTweet ? (
164 <div className="[&_.react-tweet-theme]:my-0">
165 <Tweet id={id} />
166 </div>
167 ) : provider === "image" || isInfographic ? (
168 // Image embed - display as img with proper sizing
169 <Image
170 unoptimized
171 width={400}
172 height={300}
173 src={url}
174 alt={isInfographic ? "Embedded infographic" : "Embedded image"}
175 className="h-auto w-full rounded-sm object-contain"
176 />
177 ) : (
178 // Fallback for other embed types (figma, maps, codepen, website)
179 <div className="relative w-full pb-[56.25%]">
180 <iframe
181 className="absolute top-0 left-0 size-full rounded-sm border-0"
182 title={`${provider || "embed"} embed`}
183 src={url}
184 allowFullScreen
185 allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
186 sandbox="allow-forms allow-scripts allow-popups allow-popups-to-escape-sandbox allow-presentation"
187 />
188 </div>
189 )}
190 </div>
191 </div>
192 </figure>
193 {props.children}
194 </SlateElement>
195 );
196 }
197
197 lines Plain Text