214 lines
5.7 KiB
TypeScript
214 lines
5.7 KiB
TypeScript
"use client";
|
|
import { Button, buttonVariants } from "@/components/ui/button";
|
|
import { CardTitle } from "@/components/ui/card";
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from "@/components/ui/tooltip";
|
|
import { cn } from "@/lib/utils";
|
|
import {
|
|
BookOpen,
|
|
Check,
|
|
Copy,
|
|
FileText,
|
|
Paperclip,
|
|
X,
|
|
} from "lucide-react";
|
|
import Image from "next/image";
|
|
import { useEffect, useState } from "react";
|
|
import { TrackedButton } from "./util-tracked-button";
|
|
import { TrackedLink } from "./util-tracked-link";
|
|
|
|
interface Props {
|
|
bibtexKey: string;
|
|
title: string;
|
|
authors: string[];
|
|
journal: string;
|
|
year: string;
|
|
url?: string;
|
|
pdfUrl?: string;
|
|
bibtex: string;
|
|
className?: string;
|
|
pdfAvailable?: boolean;
|
|
}
|
|
|
|
export function PublicationCard({
|
|
bibtexKey,
|
|
title,
|
|
authors,
|
|
journal,
|
|
year,
|
|
url,
|
|
pdfUrl,
|
|
bibtex,
|
|
className,
|
|
pdfAvailable = false,
|
|
}: Props) {
|
|
const [copyStatus, setCopyStatus] = useState<"idle" | "success" | "error">(
|
|
"idle"
|
|
);
|
|
const [imageError, setImageError] = useState(false);
|
|
|
|
const buttonSize = "sm";
|
|
const buttonStyle = "ghost";
|
|
|
|
|
|
const handleCopy = () => {
|
|
if (navigator.clipboard?.writeText) {
|
|
navigator.clipboard
|
|
.writeText(bibtex)
|
|
.then(() => {
|
|
setCopyStatus("success");
|
|
setTimeout(() => setCopyStatus("idle"), 2000);
|
|
})
|
|
.catch((err) => {
|
|
console.error("Failed to copy BibTeX:", err);
|
|
setCopyStatus("error");
|
|
setTimeout(() => setCopyStatus("idle"), 2000);
|
|
});
|
|
} else {
|
|
setCopyStatus("error");
|
|
setTimeout(() => setCopyStatus("idle"), 2000);
|
|
}
|
|
};
|
|
|
|
const handleDownload = () => {
|
|
const blob = new Blob([bibtex], { type: "text/plain" });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = `${bibtexKey}.bib`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url); // Clean up immediately
|
|
};
|
|
|
|
const handleCardClick = () => {
|
|
if (url) {
|
|
if (window.umami) {
|
|
window.umami.track(`${bibtexKey}-card-click`);
|
|
}
|
|
window.open(url, "_blank", "noopener,noreferrer");
|
|
}
|
|
};
|
|
|
|
const imageUrl = `/publications/${bibtexKey}.png`;
|
|
const formattedAuthors =
|
|
authors.length > 3
|
|
? `${authors.slice(0, 3).join(", ")}, et al.`
|
|
: authors.join(", ");
|
|
|
|
const ImageContent = (
|
|
<>
|
|
{!imageError ? (
|
|
<Image
|
|
src={imageUrl}
|
|
alt={`Preview for ${title}`}
|
|
fill
|
|
className="rounded-md object-cover object-top"
|
|
onError={() => setImageError(true)}
|
|
sizes="96px"
|
|
loading="lazy"
|
|
objectFit="cover"
|
|
/>
|
|
) : (
|
|
<div className="flex h-full w-full items-center justify-center rounded-md bg-muted/20">
|
|
<Paperclip className="h-6 w-6 text-muted-foreground" />
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"cards flex items-start space-x-4 rounded-lg border p-4",
|
|
url,
|
|
className
|
|
)}
|
|
id={bibtexKey}
|
|
onClick={handleCardClick}
|
|
>
|
|
<div className="pointer-events-none relative h-24 w-24 flex-shrink-0">
|
|
{ImageContent}
|
|
</div>
|
|
|
|
<div className="pointer-events-none flex-grow space-y-1">
|
|
<CardTitle className="text-base font-medium leading-snug">
|
|
<span className={cn(url && "hover:underline")}>{title}</span>
|
|
</CardTitle>
|
|
<p className="text-sm text-muted-foreground">
|
|
{formattedAuthors}. <em>{journal}</em>. {year}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="z-10 flex flex-shrink-0 flex-col space-y-1">
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<TrackedButton
|
|
variant={buttonStyle}
|
|
size={buttonSize}
|
|
className="h-7 w-7 cursor-pointer px-2 hover:border"
|
|
onClick={handleDownload}
|
|
eventName={`${bibtexKey}-bibfile`}
|
|
>
|
|
<FileText className="h-4 w-4" />
|
|
</TrackedButton>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>Download BibTeX</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<TrackedButton
|
|
variant={buttonStyle}
|
|
size={buttonSize}
|
|
className="relative h-7 w-7 cursor-pointer px-2 hover:border"
|
|
onClick={handleCopy}
|
|
eventName={`${bibtexKey}-copycite`}
|
|
>
|
|
{copyStatus === "idle" && <Copy className="h-4 w-4" />}
|
|
{copyStatus === "success" && (
|
|
<Check className="h-4 w-4 text-green-500" />
|
|
)}
|
|
{copyStatus === "error" && <X className="h-4 w-4 text-red-500" />}
|
|
</TrackedButton>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>Copy Citation</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
|
|
{pdfUrl && pdfAvailable && (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
variant={buttonStyle}
|
|
size={buttonSize}
|
|
className="h-7 w-7 px-2 hover:border"
|
|
onClick={(e) => e.stopPropagation()}
|
|
asChild
|
|
>
|
|
<TrackedLink
|
|
href={pdfUrl}
|
|
target="_blank"
|
|
eventName={`${bibtexKey}-pdf`}
|
|
rel="noopener noreferrer"
|
|
>
|
|
<BookOpen className="h-4 w-4" />
|
|
</TrackedLink>
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>Open PDF</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}; |