mirror of
https://github.com/soconnor0919/personal-website.git
synced 2025-12-12 23:04:43 -05:00
Add analytics, formatting
This commit is contained in:
57556
public/pdf.worker.min.js
vendored
57556
public/pdf.worker.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
© {new Date().getFullYear()} {name[0]?.first} {name[0]?.last}. All rights reserved.
|
||||
© {new Date().getFullYear()} {name[0]?.first}
|
||||
{name[0]?.last}. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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} />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
96
src/lib/analytics.ts
Normal 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 || "",
|
||||
});
|
||||
}
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user