"use client"; import { Node, mergeAttributes } from "@tiptap/react"; import { NodeViewWrapper, ReactNodeViewRenderer, type NodeViewProps, } from "@tiptap/react"; import { ExternalLink, Link2 } from "lucide-react"; import * as React from "react"; import { cn } from "@/lib/utils"; function mockEmbedMeta(url: string): { title: string; description: string; image: string; } { try { const u = new URL(url); const host = u.hostname.replace(/^www\./, ""); return { title: host || "Link preview", description: `Preview for ${url}`, image: `https://picsum.photos/seed/${encodeURIComponent(host)}/400/200`, }; } catch { return { title: "Link preview", description: url, image: "", }; } } function EmbedView(props: NodeViewProps) { const { node, updateAttributes, selected } = props; const url = String(node.attrs.url ?? ""); const title = String(node.attrs.title ?? ""); const description = String(node.attrs.description ?? ""); const image = String(node.attrs.image ?? ""); const [draft, setDraft] = React.useState(url); React.useEffect(() => { setDraft(url); }, [url]); const applyUrl = React.useCallback(() => { const next = draft.trim(); if (!next) return; let normalized = next; if (!/^https?:\/\//i.test(normalized)) { normalized = `https://${normalized}`; } const meta = mockEmbedMeta(normalized); updateAttributes({ url: normalized, title: meta.title, description: meta.description, image: meta.image, }); }, [draft, updateAttributes]); const onKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { e.preventDefault(); applyUrl(); } }; if (!url) { return (
setDraft(e.target.value)} onBlur={applyUrl} onKeyDown={onKeyDown} />
); } return ( {image ? ( // eslint-disable-next-line @next/next/no-img-element ) : null}
{title || url} {description ? (

{description}

) : null}

{url}

); } export const Embed = Node.create({ name: "embed", group: "block", atom: true, draggable: true, selectable: true, addAttributes() { return { url: { default: "", parseHTML: (el) => el.getAttribute("data-url") ?? "", renderHTML: (attrs) => attrs.url ? { "data-url": attrs.url } : {}, }, title: { default: "", parseHTML: (el) => el.querySelector("a")?.textContent?.trim() ?? "", renderHTML: () => ({}), }, description: { default: "", parseHTML: (el) => el.querySelector("p")?.textContent?.trim() ?? "", renderHTML: () => ({}), }, image: { default: "", parseHTML: () => "", renderHTML: () => ({}), }, }; }, parseHTML() { return [ { tag: 'div[data-type="embed"]', }, ]; }, renderHTML({ node, HTMLAttributes }) { const url = String(node.attrs.url ?? ""); const title = String(node.attrs.title ?? url); const description = String(node.attrs.description ?? ""); return [ "div", mergeAttributes(HTMLAttributes, { "data-type": "embed", ...(url ? { "data-url": url } : {}), }), [ "a", { href: url || "#", rel: "noopener noreferrer", target: "_blank", }, title, ], ["p", {}, description], ]; }, addNodeView() { return ReactNodeViewRenderer(EmbedView); }, addKeyboardShortcuts() { return { "Mod-Shift-Alt-e": () => this.editor.commands.insertContent({ type: this.name, attrs: { url: "", title: "", description: "", image: "", }, }), }; }, });