Files
llink/js/desktop/src/components/link-preview-card.tsx
T

124 lines
3.5 KiB
TypeScript

import { Copy, ExternalLink, Globe } from 'lucide-react';
import { domainFromUrl, type LinkMetadata } from '@/lib/link-metadata';
import { Skeleton } from '@/components/ui/skeleton';
import { Button } from '@/components/ui/button';
import { platform } from '@/lib/platform';
interface LinkPreviewCardProps {
metadata: LinkMetadata;
compact?: boolean;
}
export function LinkPreviewCard({ metadata, compact }: LinkPreviewCardProps) {
const handleOpen = (e: React.MouseEvent) => {
e.stopPropagation();
platform.link.openExternal(metadata.url);
};
const handleCopy = (e: React.MouseEvent) => {
e.stopPropagation();
navigator.clipboard.writeText(metadata.url);
};
return (
<div
className="bg-card text-card-foreground border-border max-w-sm overflow-hidden rounded-2xl border"
onClick={(e) => {
e.stopPropagation();
handleOpen(e);
}}
>
{metadata.image && (
<img
src={metadata.image}
alt=""
className="h-32 w-full object-cover"
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
}}
/>
)}
<div className="flex flex-col gap-1 p-3">
<div className="text-muted-foreground flex items-center gap-1.5 text-xs">
{metadata.favicon ? (
<img
src={metadata.favicon}
alt=""
className="size-4 rounded-sm"
onError={(e) => {
(e.target as HTMLImageElement).replaceWith(
document.createElement('span'),
);
}}
/>
) : (
<Globe className="size-4" />
)}
<span className="truncate">{metadata.domain}</span>
</div>
{metadata.title && (
<p className="truncate text-sm font-semibold leading-snug">
{metadata.title}
</p>
)}
{metadata.description && (
<p className="text-muted-foreground line-clamp-2 text-xs leading-relaxed">
{metadata.description}
</p>
)}
{!compact && (
<div className="mt-1.5 flex gap-2">
<Button
variant="ghost"
size="xs"
className="text-muted-foreground"
onClick={handleOpen}
>
<ExternalLink data-icon="inline-start" />
Open
</Button>
<Button
variant="ghost"
size="xs"
className="text-muted-foreground"
onClick={handleCopy}
>
<Copy data-icon="inline-start" />
Copy
</Button>
</div>
)}
</div>
</div>
);
}
/** Shown when metadata couldn't be fetched — the link itself still works. */
export function LinkPreviewCardFallback({ url }: { url: string }) {
return (
<LinkPreviewCard
metadata={{
url,
domain: domainFromUrl(url),
title: url,
description: null,
image: null,
favicon: null,
}}
/>
);
}
export function LinkPreviewCardSkeleton() {
return (
<div className="bg-card border-border max-w-sm overflow-hidden rounded-2xl border">
<Skeleton className="h-32 w-full rounded-none" />
<div className="flex flex-col gap-2 p-3">
<Skeleton className="h-3 w-24" />
<Skeleton className="h-4 w-48" />
<Skeleton className="h-3 w-full" />
</div>
</div>
);
}