137 lines
3.6 KiB
TypeScript
137 lines
3.6 KiB
TypeScript
"use client";
|
|
import React from "react";
|
|
import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Button } from "@/components/ui/button";
|
|
import { ArrowRight } from "lucide-react";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
|
|
export type LinkPreviewData = {
|
|
href: string;
|
|
title: string;
|
|
description: string;
|
|
image: string;
|
|
};
|
|
|
|
const InputLink = ({ onSubmit }: { onSubmit: (link: string) => void }) => {
|
|
const [link, setLink] = React.useState("");
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault(); // Prevent page reload
|
|
if (link.trim()) {
|
|
onSubmit(link); // Pass link to parent function
|
|
setLink(""); // Clear input field after submission
|
|
}
|
|
};
|
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
React.useEffect(() => {
|
|
inputRef.current?.focus();
|
|
}, []);
|
|
return (
|
|
<form onSubmit={handleSubmit} className="flex gap-2 " autoFocus>
|
|
<Input
|
|
ref={inputRef}
|
|
autoFocus
|
|
type="url"
|
|
placeholder="Enter a link"
|
|
value={link}
|
|
onChange={(e) => setLink(e.target.value)}
|
|
className="flex-1 focus-visible:ring-transparent border-0"
|
|
/>
|
|
<Button
|
|
type="submit"
|
|
variant={"ghost"}
|
|
size={"icon"}
|
|
className="text-muted-foreground"
|
|
>
|
|
<ArrowRight />
|
|
</Button>
|
|
</form>
|
|
);
|
|
};
|
|
|
|
const Preview = ({
|
|
href,
|
|
title,
|
|
description: _description,
|
|
image,
|
|
}: LinkPreviewData) => {
|
|
const description =
|
|
_description?.length > 100
|
|
? `${_description?.slice(0, 150)}...`
|
|
: _description;
|
|
|
|
return (
|
|
<a href={href} target="_blank">
|
|
<div className=" flex gap-4 flex-col-reverse md:flex-row">
|
|
<div className="w-full space-y-2">
|
|
<h2
|
|
className="text-xl"
|
|
style={{
|
|
margin: 0,
|
|
fontSize: "1.5rem",
|
|
}}
|
|
>
|
|
{title}
|
|
</h2>
|
|
<p className="text-sm mt-2">{description}</p>
|
|
<span className="text-xs text-muted-foreground ">{href}</span>
|
|
</div>
|
|
|
|
{image?.length ? (
|
|
<img
|
|
src={image}
|
|
alt={title}
|
|
className="w-full max-w-40 rounded-md object-cover "
|
|
/>
|
|
) : (
|
|
<div className="size-20 rounded-md bg-muted" />
|
|
)}
|
|
</div>
|
|
</a>
|
|
);
|
|
};
|
|
|
|
export const LinkPreviewComponent: React.FC<NodeViewProps> = ({
|
|
node,
|
|
updateAttributes,
|
|
extension,
|
|
}) => {
|
|
const [preview, setPreview] = React.useState<LinkPreviewData | undefined>(
|
|
(node.attrs?.href?.length && (node.attrs as LinkPreviewData)) ?? undefined
|
|
);
|
|
|
|
const [loading, setLoading] = React.useState(false);
|
|
|
|
return (
|
|
<NodeViewWrapper as="div" className="p-4 rounded-md bg-background border ">
|
|
{loading ? (
|
|
<Skeleton className="h-8 w-full rounded" />
|
|
) : preview ? (
|
|
<Preview {...preview} />
|
|
) : (
|
|
<InputLink
|
|
onSubmit={async (url: string) => {
|
|
setLoading(true);
|
|
try {
|
|
const metadata = await extension.options.fetchMetadata(url);
|
|
const newAttrs = {
|
|
href: url,
|
|
title: metadata?.title,
|
|
description: metadata?.description,
|
|
image: metadata?.image,
|
|
};
|
|
updateAttributes(newAttrs);
|
|
setPreview(newAttrs);
|
|
setLoading(false);
|
|
} catch (error) {
|
|
console.error("Error fetching metadata:", error);
|
|
setLoading(false);
|
|
}
|
|
}}
|
|
/>
|
|
)}
|
|
</NodeViewWrapper>
|
|
);
|
|
};
|