90 lines
2.4 KiB
TypeScript
90 lines
2.4 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* The link card shown under a message, and in the composer before sending.
|
|
*
|
|
* Everything rendered here was fetched by the BFF. A client can choose whether
|
|
* a card appears, never what it says — otherwise any link could be dressed up
|
|
* as any site.
|
|
*/
|
|
|
|
import { X } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
import type { LinkPreview } from "@/lib/chat";
|
|
|
|
export function LinkPreviewCard({
|
|
preview,
|
|
onDismiss,
|
|
compact = false,
|
|
className,
|
|
}: {
|
|
preview: LinkPreview;
|
|
/** When set, a dismiss control is shown (composer only). */
|
|
onDismiss?: () => void;
|
|
compact?: boolean;
|
|
className?: string;
|
|
}) {
|
|
const body = (
|
|
<div className="flex min-w-0 gap-2">
|
|
{preview.image_url && (
|
|
// Remote preview images come from arbitrary hosts, which next/image
|
|
// would need configured per-domain.
|
|
// eslint-disable-next-line @next/next/no-img-element
|
|
<img
|
|
src={preview.image_url}
|
|
alt=""
|
|
loading="lazy"
|
|
className={cn(
|
|
"shrink-0 rounded object-cover",
|
|
compact ? "size-10" : "size-14",
|
|
)}
|
|
/>
|
|
)}
|
|
<div className="min-w-0 flex-1">
|
|
{preview.site_name && (
|
|
<p className="truncate text-[11px] uppercase tracking-wide opacity-70">
|
|
{preview.site_name}
|
|
</p>
|
|
)}
|
|
<p className="line-clamp-2 text-xs font-medium">
|
|
{preview.title || preview.url}
|
|
</p>
|
|
{!compact && preview.description && (
|
|
<p className="line-clamp-2 text-[11px] opacity-80">
|
|
{preview.description}
|
|
</p>
|
|
)}
|
|
</div>
|
|
{onDismiss && (
|
|
<button
|
|
type="button"
|
|
onClick={onDismiss}
|
|
aria-label="Remove link preview"
|
|
className="shrink-0 self-start rounded-md p-1 opacity-70 hover:opacity-100"
|
|
>
|
|
<X className="size-3.5" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
const shell = cn(
|
|
"rounded-lg border-l-2 border-current/30 bg-black/5 px-2 py-1.5 dark:bg-white/10",
|
|
className,
|
|
);
|
|
|
|
// In the composer the card is being edited, so it isn't a link; under a
|
|
// message it is.
|
|
if (onDismiss) return <div className={shell}>{body}</div>;
|
|
return (
|
|
<a
|
|
href={preview.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className={cn(shell, "block hover:opacity-90")}
|
|
>
|
|
{body}
|
|
</a>
|
|
);
|
|
}
|