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() {
return (
<div className="flex flex-col items-center justify-center min-h-[calc(100vh-200px)]">
<h2 className="text-3xl font-bold mb-4">404 - Not Found</h2>
<div className="flex min-h-[calc(100vh-200px)] flex-col items-center justify-center">
<h2 className="mb-4 text-3xl font-bold">404 - Not Found</h2>
<p className="mb-6">Could not find the requested resource</p>
<Link
<Link
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
</Link>
</div>
)
}
);
}

View File

@@ -21,6 +21,11 @@ import { Skeleton } from "~/components/ui/skeleton";
import { CardSkeleton } from "~/components/ui/skeletons";
import type { Publication } from "~/lib/bibtex";
import { parseBibtex } from "~/lib/bibtex";
import {
trackPdfView,
trackDoiClick,
trackBibtexDownload,
} from "~/lib/analytics";
export default function PublicationsPage() {
const [publications, setPublications] = useState<Publication[]>([]);
@@ -38,6 +43,15 @@ export default function PublicationsPage() {
}, []);
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 {
title,
authors,
@@ -79,6 +93,41 @@ export default function PublicationsPage() {
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 (
<div className="space-y-6">
<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"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-primary sm:flex-shrink-0"
onClick={() => handlePaperClick(pub)}
>
<ArrowUpRight className="h-5 w-5" />
</Link>
@@ -157,6 +207,7 @@ export default function PublicationsPage() {
href={`https://doi.org/${pub.doi}`}
target="_blank"
rel="noopener noreferrer"
onClick={() => handleDoiClick(pub)}
>
<Badge variant="secondary" className="capitalize">
<ArrowUpRight className="mr-1 h-3 w-3" />
@@ -169,6 +220,7 @@ export default function PublicationsPage() {
href={pub.paperUrl}
target="_blank"
rel="noopener noreferrer"
onClick={() => handlePaperClick(pub)}
>
<Badge variant="secondary" className="capitalize">
<FileText className="mr-1 h-3 w-3" />
@@ -181,6 +233,7 @@ export default function PublicationsPage() {
href={pub.posterUrl}
target="_blank"
rel="noopener noreferrer"
onClick={() => handlePosterClick(pub)}
>
<Badge variant="secondary" className="capitalize">
<Presentation className="mr-1 h-3 w-3" />

View File

@@ -60,10 +60,14 @@ export function AccessibleVideo({
return (
<div className="space-y-4">
<div aria-labelledby="video-title" aria-describedby="video-description">
<h2 id="video-title" className="sr-only">{title}</h2>
<p id="video-description" className="sr-only">{description}</p>
<h2 id="video-title" className="sr-only">
{title}
</h2>
<p id="video-description" className="sr-only">
{description}
</p>
</div>
<Card className="overflow-hidden">
<CardContent className="p-0">
<div className="relative aspect-video w-full">
@@ -92,7 +96,7 @@ export function AccessibleVideo({
</div>
</CardContent>
</Card>
<div className="flex items-center justify-between">
<div className="flex gap-2">
<Button
@@ -101,25 +105,35 @@ export function AccessibleVideo({
variant="outline"
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>
</Button>
<Button
onClick={toggleMute}
aria-label={muted ? "Unmute video" : "Mute video"}
variant="outline"
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>
</Button>
</div>
{captionSrc && (
<Button
onClick={toggleCaptions}
aria-label={captionsEnabled ? "Turn off captions" : "Turn on captions"}
aria-label={
captionsEnabled ? "Turn off captions" : "Turn on captions"
}
variant="outline"
size="sm"
>
@@ -127,10 +141,12 @@ export function AccessibleVideo({
</Button>
)}
</div>
{transcript && (
<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="prose prose-sm dark:prose-invert max-w-none">
<div dangerouslySetInnerHTML={{ __html: transcript }} />
@@ -140,4 +156,4 @@ export function AccessibleVideo({
)}
</div>
);
}
}

View File

@@ -1,12 +1,13 @@
import React from 'react';
import { name } from '~/lib/data';
import React from "react";
import { name } from "~/lib/data";
export function Footer() {
return (
<footer className="lg:hidden bg-background text-foreground pb-4">
<div className="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8">
<footer className="bg-background pb-4 text-foreground lg:hidden">
<div className="mx-auto max-w-screen-xl px-4 sm:px-6 lg:px-8">
<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>
</div>
</footer>

View File

@@ -1,9 +1,9 @@
"use client"
"use client";
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cn } from "~/lib/utils"
import { cn } from "~/lib/utils";
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
@@ -13,12 +13,12 @@ const Avatar = React.forwardRef<
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
className,
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
));
Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
@@ -29,8 +29,8 @@ const AvatarImage = React.forwardRef<
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
@@ -40,11 +40,11 @@ const AvatarFallback = React.forwardRef<
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
className,
)}
{...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 { cva, type VariantProps } from "class-variance-authority"
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "~/lib/utils"
import { cn } from "~/lib/utils";
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",
@@ -20,8 +20,8 @@ const badgeVariants = cva(
defaultVariants: {
variant: "default",
},
}
)
},
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
@@ -30,7 +30,7 @@ export interface BadgeProps
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<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 { Slot } from "@radix-ui/react-slot"
import { cn } from "~/lib/utils"
import { ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons"
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cn } from "~/lib/utils";
import { ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons";
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode
separator?: React.ReactNode;
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
Breadcrumb.displayName = "Breadcrumb"
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
Breadcrumb.displayName = "Breadcrumb";
const BreadcrumbList = React.forwardRef<
HTMLOListElement,
@@ -19,12 +19,12 @@ const BreadcrumbList = React.forwardRef<
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className
className,
)}
{...props}
/>
))
BreadcrumbList.displayName = "BreadcrumbList"
));
BreadcrumbList.displayName = "BreadcrumbList";
const BreadcrumbItem = React.forwardRef<
HTMLLIElement,
@@ -35,16 +35,16 @@ const BreadcrumbItem = React.forwardRef<
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
))
BreadcrumbItem.displayName = "BreadcrumbItem"
));
BreadcrumbItem.displayName = "BreadcrumbItem";
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean
asChild?: boolean;
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
const Comp = asChild ? Slot : "a";
return (
<Comp
@@ -52,9 +52,9 @@ const BreadcrumbLink = React.forwardRef<
className={cn("transition-colors hover:text-foreground", className)}
{...props}
/>
)
})
BreadcrumbLink.displayName = "BreadcrumbLink"
);
});
BreadcrumbLink.displayName = "BreadcrumbLink";
const BreadcrumbPage = React.forwardRef<
HTMLSpanElement,
@@ -68,8 +68,8 @@ const BreadcrumbPage = React.forwardRef<
className={cn("font-normal text-foreground", className)}
{...props}
/>
))
BreadcrumbPage.displayName = "BreadcrumbPage"
));
BreadcrumbPage.displayName = "BreadcrumbPage";
const BreadcrumbSeparator = ({
children,
@@ -79,13 +79,13 @@ const BreadcrumbSeparator = ({
<li
role="presentation"
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}
>
{children ?? <ChevronRightIcon />}
</li>
)
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
);
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
const BreadcrumbEllipsis = ({
className,
@@ -100,8 +100,8 @@ const BreadcrumbEllipsis = ({
<DotsHorizontalIcon className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
)
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
);
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
export {
Breadcrumb,
@@ -111,4 +111,4 @@ export {
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}
};

View File

@@ -1,8 +1,8 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "~/lib/utils"
import { cn } from "~/lib/utils";
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",
@@ -31,27 +31,27 @@ const buttonVariants = cva(
variant: "default",
size: "default",
},
}
)
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...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<
HTMLDivElement,
@@ -10,12 +10,12 @@ const Card = React.forwardRef<
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
className,
)}
{...props}
/>
))
Card.displayName = "Card"
));
Card.displayName = "Card";
const CardHeader = React.forwardRef<
HTMLDivElement,
@@ -26,8 +26,8 @@ const CardHeader = React.forwardRef<
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
));
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<
HTMLDivElement,
@@ -38,8 +38,8 @@ const CardTitle = React.forwardRef<
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<
HTMLDivElement,
@@ -50,16 +50,16 @@ const CardDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
));
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<
HTMLDivElement,
@@ -70,7 +70,14 @@ const CardFooter = React.forwardRef<
className={cn("flex items-center p-6 pt-0", className)}
{...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 DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { cn } from "~/lib/utils"
import { CheckIcon, ChevronRightIcon, DotFilledIcon } from "@radix-ui/react-icons"
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { cn } from "~/lib/utils";
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<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
@@ -28,16 +32,16 @@ const DropdownMenuSubTrigger = React.forwardRef<
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",
inset && "pl-8",
className
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>
))
));
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
@@ -46,14 +50,14 @@ const DropdownMenuSubContent = React.forwardRef<
<DropdownMenuPrimitive.SubContent
ref={ref}
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]",
className
"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,
)}
{...props}
/>
))
));
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
@@ -65,19 +69,19 @@ const DropdownMenuContent = React.forwardRef<
sideOffset={sideOffset}
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",
"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]",
className
"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,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
@@ -85,12 +89,12 @@ const DropdownMenuItem = React.forwardRef<
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",
inset && "pl-8",
className
className,
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
@@ -100,7 +104,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
ref={ref}
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",
className
className,
)}
checked={checked}
{...props}
@@ -112,9 +116,9 @@ const DropdownMenuCheckboxItem = React.forwardRef<
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
));
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
@@ -124,7 +128,7 @@ const DropdownMenuRadioItem = React.forwardRef<
ref={ref}
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",
className
className,
)}
{...props}
>
@@ -135,13 +139,13 @@ const DropdownMenuRadioItem = React.forwardRef<
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
@@ -149,12 +153,12 @@ const DropdownMenuLabel = React.forwardRef<
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
className,
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
@@ -165,8 +169,8 @@ const DropdownMenuSeparator = React.forwardRef<
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({
className,
@@ -177,9 +181,9 @@ const DropdownMenuShortcut = ({
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
);
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export {
DropdownMenu,
@@ -197,4 +201,4 @@ export {
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}
};

View File

@@ -5,7 +5,7 @@ function Skeleton({
...props
}: React.HTMLAttributes<HTMLDivElement>) {
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 TabsPrimitive from "@radix-ui/react-tabs"
import * as React from "react";
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<
React.ElementRef<typeof TabsPrimitive.List>,
@@ -15,12 +15,12 @@ const TabsList = React.forwardRef<
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className
className,
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
@@ -30,12 +30,12 @@ const TabsTrigger = React.forwardRef<
ref={ref}
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",
className
className,
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
@@ -45,11 +45,11 @@ const TabsContent = React.forwardRef<
ref={ref}
className={cn(
"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}
/>
))
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({
subsets: ['latin'],
display: 'swap',
});
subsets: ["latin"],
display: "swap",
});

View File

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