Add analytics, formatting

This commit is contained in:
2025-08-27 10:02:41 +02:00
parent 07b71cdac9
commit fd73dde4cd
16 changed files with 57896 additions and 165 deletions

57556
public/pdf.worker.min.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -1,16 +1,16 @@
import Link from 'next/link' import Link from "next/link";
export default function NotFound() { export default function NotFound() {
return ( return (
<div className="flex flex-col items-center justify-center min-h-[calc(100vh-200px)]"> <div className="flex min-h-[calc(100vh-200px)] flex-col items-center justify-center">
<h2 className="text-3xl font-bold mb-4">404 - Not Found</h2> <h2 className="mb-4 text-3xl font-bold">404 - Not Found</h2>
<p className="mb-6">Could not find the requested resource</p> <p className="mb-6">Could not find the requested resource</p>
<Link <Link
href="/" href="/"
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90" className="rounded-md bg-primary px-4 py-2 text-primary-foreground hover:bg-primary/90"
> >
Return Home Return Home
</Link> </Link>
</div> </div>
) );
} }

View File

@@ -21,6 +21,11 @@ import { Skeleton } from "~/components/ui/skeleton";
import { CardSkeleton } from "~/components/ui/skeletons"; import { CardSkeleton } from "~/components/ui/skeletons";
import type { Publication } from "~/lib/bibtex"; import type { Publication } from "~/lib/bibtex";
import { parseBibtex } from "~/lib/bibtex"; import { parseBibtex } from "~/lib/bibtex";
import {
trackPdfView,
trackDoiClick,
trackBibtexDownload,
} from "~/lib/analytics";
export default function PublicationsPage() { export default function PublicationsPage() {
const [publications, setPublications] = useState<Publication[]>([]); const [publications, setPublications] = useState<Publication[]>([]);
@@ -38,6 +43,15 @@ export default function PublicationsPage() {
}, []); }, []);
const downloadBibtex = (pub: Publication) => { const downloadBibtex = (pub: Publication) => {
// Track the BibTeX download
trackBibtexDownload({
publicationTitle: pub.title,
publicationType: pub.type,
publicationYear: pub.year,
citationKey: pub.citationKey,
venue: pub.venue,
});
const { const {
title, title,
authors, authors,
@@ -79,6 +93,41 @@ export default function PublicationsPage() {
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
}; };
const handlePaperClick = (pub: Publication) => {
trackPdfView({
publicationTitle: pub.title,
publicationType: pub.type,
publicationYear: pub.year,
citationKey: pub.citationKey,
venue: pub.venue,
pdfType: "paper",
});
};
const handlePosterClick = (pub: Publication) => {
trackPdfView({
publicationTitle: pub.title,
publicationType: pub.type,
publicationYear: pub.year,
citationKey: pub.citationKey,
venue: pub.venue,
pdfType: "poster",
});
};
const handleDoiClick = (pub: Publication) => {
if (pub.doi) {
trackDoiClick({
publicationTitle: pub.title,
publicationType: pub.type,
publicationYear: pub.year,
citationKey: pub.citationKey,
venue: pub.venue,
doi: pub.doi,
});
}
};
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<section className="animate-fade-in-up prose prose-zinc dark:prose-invert max-w-none"> <section className="animate-fade-in-up prose prose-zinc dark:prose-invert max-w-none">
@@ -118,6 +167,7 @@ export default function PublicationsPage() {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-muted-foreground hover:text-primary sm:flex-shrink-0" className="text-muted-foreground hover:text-primary sm:flex-shrink-0"
onClick={() => handlePaperClick(pub)}
> >
<ArrowUpRight className="h-5 w-5" /> <ArrowUpRight className="h-5 w-5" />
</Link> </Link>
@@ -157,6 +207,7 @@ export default function PublicationsPage() {
href={`https://doi.org/${pub.doi}`} href={`https://doi.org/${pub.doi}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
onClick={() => handleDoiClick(pub)}
> >
<Badge variant="secondary" className="capitalize"> <Badge variant="secondary" className="capitalize">
<ArrowUpRight className="mr-1 h-3 w-3" /> <ArrowUpRight className="mr-1 h-3 w-3" />
@@ -169,6 +220,7 @@ export default function PublicationsPage() {
href={pub.paperUrl} href={pub.paperUrl}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
onClick={() => handlePaperClick(pub)}
> >
<Badge variant="secondary" className="capitalize"> <Badge variant="secondary" className="capitalize">
<FileText className="mr-1 h-3 w-3" /> <FileText className="mr-1 h-3 w-3" />
@@ -181,6 +233,7 @@ export default function PublicationsPage() {
href={pub.posterUrl} href={pub.posterUrl}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
onClick={() => handlePosterClick(pub)}
> >
<Badge variant="secondary" className="capitalize"> <Badge variant="secondary" className="capitalize">
<Presentation className="mr-1 h-3 w-3" /> <Presentation className="mr-1 h-3 w-3" />

View File

@@ -60,10 +60,14 @@ export function AccessibleVideo({
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div aria-labelledby="video-title" aria-describedby="video-description"> <div aria-labelledby="video-title" aria-describedby="video-description">
<h2 id="video-title" className="sr-only">{title}</h2> <h2 id="video-title" className="sr-only">
<p id="video-description" className="sr-only">{description}</p> {title}
</h2>
<p id="video-description" className="sr-only">
{description}
</p>
</div> </div>
<Card className="overflow-hidden"> <Card className="overflow-hidden">
<CardContent className="p-0"> <CardContent className="p-0">
<div className="relative aspect-video w-full"> <div className="relative aspect-video w-full">
@@ -92,7 +96,7 @@ export function AccessibleVideo({
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex gap-2"> <div className="flex gap-2">
<Button <Button
@@ -101,25 +105,35 @@ export function AccessibleVideo({
variant="outline" variant="outline"
size="sm" size="sm"
> >
{playing ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4" />} {playing ? (
<Pause className="h-4 w-4" />
) : (
<Play className="h-4 w-4" />
)}
<span className="ml-2">{playing ? "Pause" : "Play"}</span> <span className="ml-2">{playing ? "Pause" : "Play"}</span>
</Button> </Button>
<Button <Button
onClick={toggleMute} onClick={toggleMute}
aria-label={muted ? "Unmute video" : "Mute video"} aria-label={muted ? "Unmute video" : "Mute video"}
variant="outline" variant="outline"
size="sm" size="sm"
> >
{muted ? <VolumeX className="h-4 w-4" /> : <Volume2 className="h-4 w-4" />} {muted ? (
<VolumeX className="h-4 w-4" />
) : (
<Volume2 className="h-4 w-4" />
)}
<span className="ml-2">{muted ? "Unmute" : "Mute"}</span> <span className="ml-2">{muted ? "Unmute" : "Mute"}</span>
</Button> </Button>
</div> </div>
{captionSrc && ( {captionSrc && (
<Button <Button
onClick={toggleCaptions} onClick={toggleCaptions}
aria-label={captionsEnabled ? "Turn off captions" : "Turn on captions"} aria-label={
captionsEnabled ? "Turn off captions" : "Turn on captions"
}
variant="outline" variant="outline"
size="sm" size="sm"
> >
@@ -127,10 +141,12 @@ export function AccessibleVideo({
</Button> </Button>
)} )}
</div> </div>
{transcript && ( {transcript && (
<details className="mt-4"> <details className="mt-4">
<summary className="cursor-pointer font-medium text-primary">View Full Transcript</summary> <summary className="cursor-pointer font-medium text-primary">
View Full Transcript
</summary>
<div className="mt-2 rounded-md bg-muted p-4"> <div className="mt-2 rounded-md bg-muted p-4">
<div className="prose prose-sm dark:prose-invert max-w-none"> <div className="prose prose-sm dark:prose-invert max-w-none">
<div dangerouslySetInnerHTML={{ __html: transcript }} /> <div dangerouslySetInnerHTML={{ __html: transcript }} />
@@ -140,4 +156,4 @@ export function AccessibleVideo({
)} )}
</div> </div>
); );
} }

View File

@@ -1,12 +1,13 @@
import React from 'react'; import React from "react";
import { name } from '~/lib/data'; import { name } from "~/lib/data";
export function Footer() { export function Footer() {
return ( return (
<footer className="lg:hidden bg-background text-foreground pb-4"> <footer className="bg-background pb-4 text-foreground lg:hidden">
<div className="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="mx-auto max-w-screen-xl px-4 sm:px-6 lg:px-8">
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
&copy; {new Date().getFullYear()} {name[0]?.first}&nbsp;{name[0]?.last}. All rights reserved. &copy; {new Date().getFullYear()} {name[0]?.first}&nbsp;
{name[0]?.last}. All rights reserved.
</p> </p>
</div> </div>
</footer> </footer>

View File

@@ -1,9 +1,9 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar" import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cn } from "~/lib/utils" import { cn } from "~/lib/utils";
const Avatar = React.forwardRef< const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>, React.ElementRef<typeof AvatarPrimitive.Root>,
@@ -13,12 +13,12 @@ const Avatar = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className className,
)} )}
{...props} {...props}
/> />
)) ));
Avatar.displayName = AvatarPrimitive.Root.displayName Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef< const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>, React.ElementRef<typeof AvatarPrimitive.Image>,
@@ -29,8 +29,8 @@ const AvatarImage = React.forwardRef<
className={cn("aspect-square h-full w-full", className)} className={cn("aspect-square h-full w-full", className)}
{...props} {...props}
/> />
)) ));
AvatarImage.displayName = AvatarPrimitive.Image.displayName AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef< const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>, React.ElementRef<typeof AvatarPrimitive.Fallback>,
@@ -40,11 +40,11 @@ const AvatarFallback = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted", "flex h-full w-full items-center justify-center rounded-full bg-muted",
className className,
)} )}
{...props} {...props}
/> />
)) ));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
export { Avatar, AvatarImage, AvatarFallback } export { Avatar, AvatarImage, AvatarFallback };

View File

@@ -1,7 +1,7 @@
import * as React from "react" import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "~/lib/utils" import { cn } from "~/lib/utils";
const badgeVariants = cva( const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
@@ -20,8 +20,8 @@ const badgeVariants = cva(
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
}, },
} },
) );
export interface BadgeProps export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>, extends React.HTMLAttributes<HTMLDivElement>,
@@ -30,7 +30,7 @@ export interface BadgeProps
function Badge({ className, variant, ...props }: BadgeProps) { function Badge({ className, variant, ...props }: BadgeProps) {
return ( return (
<div className={cn(badgeVariants({ variant }), className)} {...props} /> <div className={cn(badgeVariants({ variant }), className)} {...props} />
) );
} }
export { Badge, badgeVariants } export { Badge, badgeVariants };

View File

@@ -1,15 +1,15 @@
import * as React from "react" import * as React from "react";
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot";
import { cn } from "~/lib/utils" import { cn } from "~/lib/utils";
import { ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons" import { ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons";
const Breadcrumb = React.forwardRef< const Breadcrumb = React.forwardRef<
HTMLElement, HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & { React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode separator?: React.ReactNode;
} }
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />) >(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
Breadcrumb.displayName = "Breadcrumb" Breadcrumb.displayName = "Breadcrumb";
const BreadcrumbList = React.forwardRef< const BreadcrumbList = React.forwardRef<
HTMLOListElement, HTMLOListElement,
@@ -19,12 +19,12 @@ const BreadcrumbList = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5", "flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className className,
)} )}
{...props} {...props}
/> />
)) ));
BreadcrumbList.displayName = "BreadcrumbList" BreadcrumbList.displayName = "BreadcrumbList";
const BreadcrumbItem = React.forwardRef< const BreadcrumbItem = React.forwardRef<
HTMLLIElement, HTMLLIElement,
@@ -35,16 +35,16 @@ const BreadcrumbItem = React.forwardRef<
className={cn("inline-flex items-center gap-1.5", className)} className={cn("inline-flex items-center gap-1.5", className)}
{...props} {...props}
/> />
)) ));
BreadcrumbItem.displayName = "BreadcrumbItem" BreadcrumbItem.displayName = "BreadcrumbItem";
const BreadcrumbLink = React.forwardRef< const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement, HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & { React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean asChild?: boolean;
} }
>(({ asChild, className, ...props }, ref) => { >(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a" const Comp = asChild ? Slot : "a";
return ( return (
<Comp <Comp
@@ -52,9 +52,9 @@ const BreadcrumbLink = React.forwardRef<
className={cn("transition-colors hover:text-foreground", className)} className={cn("transition-colors hover:text-foreground", className)}
{...props} {...props}
/> />
) );
}) });
BreadcrumbLink.displayName = "BreadcrumbLink" BreadcrumbLink.displayName = "BreadcrumbLink";
const BreadcrumbPage = React.forwardRef< const BreadcrumbPage = React.forwardRef<
HTMLSpanElement, HTMLSpanElement,
@@ -68,8 +68,8 @@ const BreadcrumbPage = React.forwardRef<
className={cn("font-normal text-foreground", className)} className={cn("font-normal text-foreground", className)}
{...props} {...props}
/> />
)) ));
BreadcrumbPage.displayName = "BreadcrumbPage" BreadcrumbPage.displayName = "BreadcrumbPage";
const BreadcrumbSeparator = ({ const BreadcrumbSeparator = ({
children, children,
@@ -79,13 +79,13 @@ const BreadcrumbSeparator = ({
<li <li
role="presentation" role="presentation"
aria-hidden="true" aria-hidden="true"
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)} className={cn("[&>svg]:h-3.5 [&>svg]:w-3.5", className)}
{...props} {...props}
> >
{children ?? <ChevronRightIcon />} {children ?? <ChevronRightIcon />}
</li> </li>
) );
BreadcrumbSeparator.displayName = "BreadcrumbSeparator" BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
const BreadcrumbEllipsis = ({ const BreadcrumbEllipsis = ({
className, className,
@@ -100,8 +100,8 @@ const BreadcrumbEllipsis = ({
<DotsHorizontalIcon className="h-4 w-4" /> <DotsHorizontalIcon className="h-4 w-4" />
<span className="sr-only">More</span> <span className="sr-only">More</span>
</span> </span>
) );
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis" BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
export { export {
Breadcrumb, Breadcrumb,
@@ -111,4 +111,4 @@ export {
BreadcrumbPage, BreadcrumbPage,
BreadcrumbSeparator, BreadcrumbSeparator,
BreadcrumbEllipsis, BreadcrumbEllipsis,
} };

View File

@@ -1,8 +1,8 @@
import * as React from "react" import * as React from "react";
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "~/lib/utils" import { cn } from "~/lib/utils";
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
@@ -31,27 +31,27 @@ const buttonVariants = cva(
variant: "default", variant: "default",
size: "default", size: "default",
}, },
} },
) );
export interface ButtonProps export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>, extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> { VariantProps<typeof buttonVariants> {
asChild?: boolean asChild?: boolean;
} }
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => { ({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button" const Comp = asChild ? Slot : "button";
return ( return (
<Comp <Comp
className={cn(buttonVariants({ variant, size, className }))} className={cn(buttonVariants({ variant, size, className }))}
ref={ref} ref={ref}
{...props} {...props}
/> />
) );
} },
) );
Button.displayName = "Button" Button.displayName = "Button";
export { Button, buttonVariants } export { Button, buttonVariants };

View File

@@ -1,6 +1,6 @@
import * as React from "react" import * as React from "react";
import { cn } from "~/lib/utils" import { cn } from "~/lib/utils";
const Card = React.forwardRef< const Card = React.forwardRef<
HTMLDivElement, HTMLDivElement,
@@ -10,12 +10,12 @@ const Card = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"rounded-xl border bg-card text-card-foreground shadow", "rounded-xl border bg-card text-card-foreground shadow",
className className,
)} )}
{...props} {...props}
/> />
)) ));
Card.displayName = "Card" Card.displayName = "Card";
const CardHeader = React.forwardRef< const CardHeader = React.forwardRef<
HTMLDivElement, HTMLDivElement,
@@ -26,8 +26,8 @@ const CardHeader = React.forwardRef<
className={cn("flex flex-col space-y-1.5 p-6", className)} className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props} {...props}
/> />
)) ));
CardHeader.displayName = "CardHeader" CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef< const CardTitle = React.forwardRef<
HTMLDivElement, HTMLDivElement,
@@ -38,8 +38,8 @@ const CardTitle = React.forwardRef<
className={cn("font-semibold leading-none tracking-tight", className)} className={cn("font-semibold leading-none tracking-tight", className)}
{...props} {...props}
/> />
)) ));
CardTitle.displayName = "CardTitle" CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef< const CardDescription = React.forwardRef<
HTMLDivElement, HTMLDivElement,
@@ -50,16 +50,16 @@ const CardDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)} className={cn("text-sm text-muted-foreground", className)}
{...props} {...props}
/> />
)) ));
CardDescription.displayName = "CardDescription" CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef< const CardContent = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} /> <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
)) ));
CardContent.displayName = "CardContent" CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef< const CardFooter = React.forwardRef<
HTMLDivElement, HTMLDivElement,
@@ -70,7 +70,14 @@ const CardFooter = React.forwardRef<
className={cn("flex items-center p-6 pt-0", className)} className={cn("flex items-center p-6 pt-0", className)}
{...props} {...props}
/> />
)) ));
CardFooter.displayName = "CardFooter" CardFooter.displayName = "CardFooter";
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
};

View File

@@ -1,26 +1,30 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { cn } from "~/lib/utils" import { cn } from "~/lib/utils";
import { CheckIcon, ChevronRightIcon, DotFilledIcon } from "@radix-ui/react-icons" import {
CheckIcon,
ChevronRightIcon,
DotFilledIcon,
} from "@radix-ui/react-icons";
const DropdownMenu = DropdownMenuPrimitive.Root const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef< const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>, React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & { React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean inset?: boolean;
} }
>(({ className, inset, children, ...props }, ref) => ( >(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger <DropdownMenuPrimitive.SubTrigger
@@ -28,16 +32,16 @@ const DropdownMenuSubTrigger = React.forwardRef<
className={cn( className={cn(
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", "flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8", inset && "pl-8",
className className,
)} )}
{...props} {...props}
> >
{children} {children}
<ChevronRightIcon className="ml-auto" /> <ChevronRightIcon className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger> </DropdownMenuPrimitive.SubTrigger>
)) ));
DropdownMenuSubTrigger.displayName = DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef< const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>, React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
@@ -46,14 +50,14 @@ const DropdownMenuSubContent = React.forwardRef<
<DropdownMenuPrimitive.SubContent <DropdownMenuPrimitive.SubContent
ref={ref} ref={ref}
className={cn( className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]", "z-50 min-w-[8rem] origin-[--radix-dropdown-menu-content-transform-origin] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className className,
)} )}
{...props} {...props}
/> />
)) ));
DropdownMenuSubContent.displayName = DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef< const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>, React.ElementRef<typeof DropdownMenuPrimitive.Content>,
@@ -65,19 +69,19 @@ const DropdownMenuContent = React.forwardRef<
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md", "z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]", "origin-[--radix-dropdown-menu-content-transform-origin] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className className,
)} )}
{...props} {...props}
/> />
</DropdownMenuPrimitive.Portal> </DropdownMenuPrimitive.Portal>
)) ));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef< const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>, React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & { React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean inset?: boolean;
} }
>(({ className, inset, ...props }, ref) => ( >(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item <DropdownMenuPrimitive.Item
@@ -85,12 +89,12 @@ const DropdownMenuItem = React.forwardRef<
className={cn( className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0", "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
inset && "pl-8", inset && "pl-8",
className className,
)} )}
{...props} {...props}
/> />
)) ));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef< const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>, React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
@@ -100,7 +104,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className className,
)} )}
checked={checked} checked={checked}
{...props} {...props}
@@ -112,9 +116,9 @@ const DropdownMenuCheckboxItem = React.forwardRef<
</span> </span>
{children} {children}
</DropdownMenuPrimitive.CheckboxItem> </DropdownMenuPrimitive.CheckboxItem>
)) ));
DropdownMenuCheckboxItem.displayName = DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef< const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>, React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
@@ -124,7 +128,7 @@ const DropdownMenuRadioItem = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className className,
)} )}
{...props} {...props}
> >
@@ -135,13 +139,13 @@ const DropdownMenuRadioItem = React.forwardRef<
</span> </span>
{children} {children}
</DropdownMenuPrimitive.RadioItem> </DropdownMenuPrimitive.RadioItem>
)) ));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef< const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>, React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & { React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean inset?: boolean;
} }
>(({ className, inset, ...props }, ref) => ( >(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label <DropdownMenuPrimitive.Label
@@ -149,12 +153,12 @@ const DropdownMenuLabel = React.forwardRef<
className={cn( className={cn(
"px-2 py-1.5 text-sm font-semibold", "px-2 py-1.5 text-sm font-semibold",
inset && "pl-8", inset && "pl-8",
className className,
)} )}
{...props} {...props}
/> />
)) ));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef< const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>, React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
@@ -165,8 +169,8 @@ const DropdownMenuSeparator = React.forwardRef<
className={cn("-mx-1 my-1 h-px bg-muted", className)} className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props} {...props}
/> />
)) ));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({ const DropdownMenuShortcut = ({
className, className,
@@ -177,9 +181,9 @@ const DropdownMenuShortcut = ({
className={cn("ml-auto text-xs tracking-widest opacity-60", className)} className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props} {...props}
/> />
) );
} };
DropdownMenuShortcut.displayName = "DropdownMenuShortcut" DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export { export {
DropdownMenu, DropdownMenu,
@@ -197,4 +201,4 @@ export {
DropdownMenuSubContent, DropdownMenuSubContent,
DropdownMenuSubTrigger, DropdownMenuSubTrigger,
DropdownMenuRadioGroup, DropdownMenuRadioGroup,
} };

View File

@@ -5,7 +5,7 @@ function Skeleton({
...props ...props
}: React.HTMLAttributes<HTMLDivElement>) { }: React.HTMLAttributes<HTMLDivElement>) {
return ( return (
<div className={cn("bg-primary/10 animate-pulse", className)} {...props} /> <div className={cn("animate-pulse bg-primary/10", className)} {...props} />
); );
} }

View File

@@ -1,11 +1,11 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs" import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "~/lib/utils" import { cn } from "~/lib/utils";
const Tabs = TabsPrimitive.Root const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef< const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>, React.ElementRef<typeof TabsPrimitive.List>,
@@ -15,12 +15,12 @@ const TabsList = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground", "inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className className,
)} )}
{...props} {...props}
/> />
)) ));
TabsList.displayName = TabsPrimitive.List.displayName TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef< const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>, React.ElementRef<typeof TabsPrimitive.Trigger>,
@@ -30,12 +30,12 @@ const TabsTrigger = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow", "inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
className className,
)} )}
{...props} {...props}
/> />
)) ));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef< const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>, React.ElementRef<typeof TabsPrimitive.Content>,
@@ -45,11 +45,11 @@ const TabsContent = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className className,
)} )}
{...props} {...props}
/> />
)) ));
TabsContent.displayName = TabsPrimitive.Content.displayName TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent } export { Tabs, TabsList, TabsTrigger, TabsContent };

96
src/lib/analytics.ts Normal file
View File

@@ -0,0 +1,96 @@
import { track } from "@vercel/analytics";
/**
* Analytics tracking utilities for publication interactions
*
* This module provides tracking functions for various publication-related actions
* using Vercel Analytics. It tracks paper downloads, DOI clicks, BibTeX downloads,
* and other publication interactions with detailed metadata.
*
* @example
* ```typescript
* import { trackPdfView } from '~/lib/analytics';
*
* // Track when a user clicks on a paper PDF link
* trackPdfView({
* publicationTitle: "My Research Paper",
* publicationType: "conference",
* publicationYear: 2024,
* citationKey: "Author2024",
* venue: "IEEE Conference",
* pdfType: "paper"
* });
* ```
*/
export interface PublicationTrackingData {
publicationTitle: string;
publicationType: "conference" | "journal" | "workshop" | "thesis";
publicationYear: number;
linkType: "paper" | "poster" | "doi" | "bibtex";
citationKey?: string;
venue?: string;
}
/**
* Track publication link clicks with detailed metadata
*/
export function trackPublicationLink(data: PublicationTrackingData) {
track("Publication Link Click", {
title: data.publicationTitle,
type: data.publicationType,
year: data.publicationYear.toString(),
link_type: data.linkType,
citation_key: data.citationKey || "",
venue: data.venue || "",
});
}
/**
* Track BibTeX downloads specifically
*/
export function trackBibtexDownload(
data: Omit<PublicationTrackingData, "linkType">,
) {
track("BibTeX Download", {
title: data.publicationTitle,
type: data.publicationType,
year: data.publicationYear.toString(),
citation_key: data.citationKey || "",
venue: data.venue || "",
});
}
/**
* Track PDF views (paper or poster)
*/
export function trackPdfView(
data: Omit<PublicationTrackingData, "linkType"> & {
pdfType: "paper" | "poster";
},
) {
track("Publication PDF View", {
title: data.publicationTitle,
type: data.publicationType,
year: data.publicationYear.toString(),
pdf_type: data.pdfType,
citation_key: data.citationKey || "",
venue: data.venue || "",
});
}
/**
* Track DOI clicks
*/
export function trackDoiClick(
data: Omit<PublicationTrackingData, "linkType"> & { doi: string },
) {
track("DOI Link Click", {
title: data.publicationTitle,
type: data.publicationType,
year: data.publicationYear.toString(),
doi: data.doi,
citation_key: data.citationKey || "",
venue: data.venue || "",
});
}

View File

@@ -1,6 +1,6 @@
import { Inter } from 'next/font/google' import { Inter } from "next/font/google";
export const inter = Inter({ export const inter = Inter({
subsets: ['latin'], subsets: ["latin"],
display: 'swap', display: "swap",
}); });

View File

@@ -1,6 +1,6 @@
import { clsx, type ClassValue } from "clsx" import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge" import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs));
} }