CV Caching, new dashboard

This commit is contained in:
2025-08-01 02:05:35 -04:00
parent 9609ed7eef
commit c7ce82ec36
21 changed files with 1665 additions and 11160 deletions

View File

@@ -0,0 +1,117 @@
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const url = searchParams.get("url");
if (!url) {
return NextResponse.json(
{
error: "URL parameter is required",
usage: "Add ?url=<github-release-url> to test the proxy",
example:
"/api/pdf-proxy?url=https://github.com/user/repo/releases/download/latest/file.pdf",
},
{ status: 400 },
);
}
// Validate that the URL is from GitHub releases
if (!url.includes("github.com") || !url.includes("/releases/download/")) {
return NextResponse.json(
{ error: "Only GitHub release URLs are allowed" },
{ status: 403 },
);
}
try {
// Follow redirects and fetch the actual file
const response = await fetch(url, {
headers: {
"User-Agent": "Mozilla/5.0 (compatible; PDF-Proxy/1.0)",
Accept: "application/pdf,*/*",
},
redirect: "follow", // Follow redirects
});
if (!response.ok) {
return NextResponse.json(
{ error: `Failed to fetch PDF: ${response.status}` },
{ status: response.status },
);
}
// Get the response buffer first
const pdfBuffer = await response.arrayBuffer();
// Check if it's actually a PDF by looking at the file signature
const uint8Array = new Uint8Array(pdfBuffer);
// More robust PDF signature check
const firstBytes = Array.from(uint8Array.slice(0, 10));
const pdfSignature = String.fromCharCode(...uint8Array.slice(0, 4));
const extendedSignature = String.fromCharCode(...uint8Array.slice(0, 8));
// Only log in development
if (process.env.NODE_ENV === "development") {
console.log("PDF proxy - URL:", response.url);
console.log("PDF proxy - Size:", pdfBuffer.byteLength, "bytes");
console.log("PDF proxy - Signature:", JSON.stringify(pdfSignature));
}
// Check for PDF signature - should start with "%PDF"
const isPDF =
pdfSignature === "%PDF" ||
(uint8Array[0] === 0x25 && // %
uint8Array[1] === 0x50 && // P
uint8Array[2] === 0x44 && // D
uint8Array[3] === 0x46); // F
if (!isPDF) {
console.error(
"PDF proxy - Invalid signature:",
JSON.stringify(pdfSignature),
);
return NextResponse.json(
{
error: `Resource is not a valid PDF file. Got signature: ${JSON.stringify(pdfSignature)}`,
debug: {
signature: pdfSignature,
firstBytes: firstBytes.slice(0, 4),
size: pdfBuffer.byteLength,
},
},
{ status: 400 },
);
}
// Also check content-type as secondary validation (but be more lenient)
const contentType = response.headers.get("content-type");
return new NextResponse(pdfBuffer, {
headers: {
"Content-Type": "application/pdf",
"Content-Length": pdfBuffer.byteLength.toString(),
"Cache-Control": "public, max-age=7200", // Cache for 2 hours
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET",
"Access-Control-Allow-Headers": "Content-Type",
"Content-Disposition": "inline", // Display in browser instead of download
},
});
} catch (error) {
console.error("PDF proxy error:", error);
return NextResponse.json({ error: "Failed to proxy PDF" }, { status: 500 });
}
}
export async function OPTIONS() {
return new NextResponse(null, {
status: 200,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET",
"Access-Control-Allow-Headers": "Content-Type",
},
});
}

View File

@@ -1,108 +1,534 @@
'use client';
"use client";
import { useState } from 'react';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '~/components/ui/tabs';
import { Badge } from '~/components/ui/badge';
import { Card, CardHeader, CardContent, CardTitle, CardDescription } from '~/components/ui/card';
import { Download } from 'lucide-react';
import Link from 'next/link';
import { useState, useRef, useEffect, useCallback } from "react";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "~/components/ui/tabs";
import {
Card,
CardHeader,
CardContent,
CardTitle,
CardDescription,
} from "~/components/ui/card";
import { Button } from "~/components/ui/button";
import {
Download,
ExternalLink,
FileText,
ZoomIn,
ZoomOut,
RotateCw,
ChevronLeft,
ChevronRight,
AlertCircle,
Loader2,
Eye,
} from "lucide-react";
import Link from "next/link";
import type { PDFDocumentProxy, PDFPageProxy } from "pdfjs-dist";
// GitHub release URLs for PDFs
const CV_URL = "https://github.com/soconnor0919/resume-cv/releases/download/latest/cv.pdf";
const RESUME_URL = "https://github.com/soconnor0919/resume-cv/releases/download/latest/resume.pdf";
const CV_URL =
"https://github.com/soconnor0919/resume-cv/releases/download/latest/cv.pdf";
const RESUME_URL =
"https://github.com/soconnor0919/resume-cv/releases/download/latest/resume.pdf";
interface PDFViewerProps {
url: string;
title: string;
type: "cv" | "resume";
}
function PDFViewer({ url, title, type }: PDFViewerProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [pdfDoc, setPdfDoc] = useState<PDFDocumentProxy | null>(null);
const [pageNum, setPageNum] = useState(1);
const [pageCount, setPageCount] = useState(0);
const [scale, setScale] = useState(1.2);
const [rotation, setRotation] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const [hasError, setHasError] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
const [isClient, setIsClient] = useState(false);
const [pdfBlob, setPdfBlob] = useState<Uint8Array | null>(null);
useEffect(() => {
setIsClient(true);
}, []);
// Download PDF and convert to blob for PDF.js
const downloadPDF = async (pdfUrl: string): Promise<Uint8Array> => {
try {
// Try direct fetch first
const response = await fetch(pdfUrl, {
mode: "cors",
headers: {
Accept: "application/pdf",
},
});
if (!response.ok) {
throw new Error(`Failed to download PDF: ${response.status}`);
}
const arrayBuffer = await response.arrayBuffer();
return new Uint8Array(arrayBuffer);
} catch (error) {
// If direct fetch fails (e.g., CORS), try proxy
console.warn("Direct fetch failed, trying proxy:", error);
const proxyUrl = `/api/pdf-proxy?url=${encodeURIComponent(pdfUrl)}`;
const response = await fetch(proxyUrl);
if (!response.ok) {
throw new Error(`Failed to download PDF via proxy: ${response.status}`);
}
const arrayBuffer = await response.arrayBuffer();
return new Uint8Array(arrayBuffer);
}
};
useEffect(() => {
if (!isClient) return;
const loadPDF = async () => {
try {
setIsLoading(true);
setHasError(false);
// Download the PDF first
const pdfData = await downloadPDF(url);
setPdfBlob(pdfData);
// Dynamically import PDF.js
const pdfjsLib = await import("pdfjs-dist");
// Set worker path
pdfjsLib.GlobalWorkerOptions.workerSrc = "/pdf.worker.min.js";
// Load PDF from the downloaded data
const loadingTask = pdfjsLib.getDocument({ data: pdfData });
const pdf = await loadingTask.promise;
setPdfDoc(pdf);
setPageCount(pdf.numPages);
setPageNum(1);
} catch (error) {
console.error("Error loading PDF:", error);
setHasError(true);
setErrorMessage(
error instanceof Error
? error.message
: "Failed to load PDF. Please try downloading it directly.",
);
} finally {
setIsLoading(false);
}
};
loadPDF();
}, [url, isClient]);
useEffect(() => {
const renderPage = async () => {
if (!pdfDoc || !canvasRef.current) return;
try {
const page: PDFPageProxy = await pdfDoc.getPage(pageNum);
const canvas = canvasRef.current;
const context = canvas.getContext("2d");
if (!context) return;
// Get container width for responsive scaling
const container = canvas.parentElement;
const containerWidth = container
? container.clientWidth - 32
: window.innerWidth - 32; // Account for padding
// Calculate responsive scale
const baseViewport = page.getViewport({ scale: 1, rotation });
const responsiveScale = Math.min(
containerWidth / baseViewport.width,
scale,
);
const viewport = page.getViewport({ scale: responsiveScale, rotation });
// Fix blurriness by using device pixel ratio
const devicePixelRatio = window.devicePixelRatio || 1;
const outputScale = devicePixelRatio;
canvas.width = Math.floor(viewport.width * outputScale);
canvas.height = Math.floor(viewport.height * outputScale);
canvas.style.width = Math.floor(viewport.width) + "px";
canvas.style.height = Math.floor(viewport.height) + "px";
// Enable anti-aliasing and smoothing
context.imageSmoothingEnabled = true;
context.imageSmoothingQuality = "high";
const transform =
outputScale !== 1
? [outputScale, 0, 0, outputScale, 0, 0]
: undefined;
const renderContext = {
canvasContext: context,
viewport: viewport,
transform: transform,
};
await page.render(renderContext).promise;
} catch (error) {
console.error("Error rendering page:", error);
setHasError(true);
setErrorMessage("Failed to render PDF page.");
}
};
renderPage();
}, [pdfDoc, pageNum, scale, rotation]);
const nextPage = () => {
if (pageNum < pageCount) {
setPageNum(pageNum + 1);
}
};
const prevPage = () => {
if (pageNum > 1) {
setPageNum(pageNum - 1);
}
};
const zoomIn = () => {
if (scale < 3) {
setScale(scale + 0.2);
}
};
const zoomOut = () => {
if (scale > 0.5) {
setScale(scale - 0.2);
}
};
const rotate = () => {
setRotation((rotation + 90) % 360);
};
const downloadBlob = () => {
if (!pdfBlob) return;
const blob = new Blob([pdfBlob], { type: "application/pdf" });
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${title.toLowerCase().replace(/\s+/g, "_")}.pdf`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
};
if (!isClient) {
return (
<div className="space-y-4">
{/* Action Bar */}
<div className="bg-muted/50 flex items-center justify-between gap-4 rounded-lg border p-4">
<div className="flex items-center gap-2">
<FileText className="h-5 w-5 text-muted-foreground" />
<div>
<h3 className="font-medium">{title}</h3>
<p className="text-sm text-muted-foreground">
{type === "cv"
? "Academic curriculum vitae"
: "Professional resume"}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" className="gap-2" disabled>
<Download className="h-4 w-4" />
<span className="hidden sm:inline">Download</span>
</Button>
<Button variant="outline" size="sm" className="gap-2" disabled>
<ExternalLink className="h-4 w-4" />
<span className="hidden sm:inline">Open</span>
</Button>
</div>
</div>
{/* Loading State */}
<div className="relative overflow-hidden rounded-lg border bg-background">
<div className="flex justify-center p-4">
<div className="h-[600px] w-full max-w-2xl animate-pulse rounded-lg bg-muted lg:h-[800px]" />
</div>
</div>
</div>
);
}
if (hasError) {
return (
<div className="space-y-4">
{/* Action Bar */}
<div className="bg-muted/50 flex items-center justify-between gap-4 rounded-lg border p-4">
<div className="flex items-center gap-2">
<FileText className="h-5 w-5 text-muted-foreground" />
<h3 className="font-medium">{title}</h3>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
className="gap-2"
onClick={pdfBlob ? downloadBlob : undefined}
disabled={!pdfBlob}
>
<Download className="h-4 w-4" />
<span className="hidden sm:inline">Download</span>
</Button>
<Button variant="outline" size="sm" asChild className="gap-2">
<Link
href={`/api/pdf-proxy?url=${encodeURIComponent(url)}`}
target="_blank"
rel="noopener noreferrer"
>
<Eye className="h-4 w-4" />
<span className="hidden sm:inline">View PDF</span>
</Link>
</Button>
</div>
</div>
<Card>
<CardHeader className="pb-2">
<div className="flex items-center gap-2">
<AlertCircle className="h-5 w-5 text-destructive" />
<CardTitle className="text-base">Unable to load PDF</CardTitle>
</div>
<CardDescription>{errorMessage}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-wrap gap-2">
<Button
variant="default"
size="sm"
className="gap-2"
onClick={pdfBlob ? downloadBlob : undefined}
disabled={!pdfBlob}
>
<Download className="h-4 w-4" />
<span className="hidden sm:inline">Download {title}</span>
</Button>
<Button variant="outline" size="sm" asChild className="gap-2">
<Link
href={`/api/pdf-proxy?url=${encodeURIComponent(url)}`}
target="_blank"
rel="noopener noreferrer"
>
<Eye className="h-4 w-4" />
<span className="hidden sm:inline">View PDF in New Tab</span>
</Link>
</Button>
</div>
<div className="rounded-md border border-dashed p-4 text-center">
<p className="text-sm text-muted-foreground">
PDF preview not available. Please use the download or open
buttons above.
</p>
</div>
</CardContent>
</Card>
</div>
);
}
return (
<div className="space-y-4">
{/* Action Bar */}
<div className="bg-muted/50 flex items-center justify-between gap-4 rounded-lg border p-4">
<div className="flex items-center gap-2">
<FileText className="h-5 w-5 text-muted-foreground" />
<h3 className="font-medium">{title}</h3>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
className="gap-2"
onClick={downloadBlob}
disabled={!pdfBlob}
>
<Download className="h-4 w-4" />
<span className="hidden sm:inline">Download</span>
</Button>
<Button variant="outline" size="sm" asChild className="gap-2">
<Link
href={`/api/pdf-proxy?url=${encodeURIComponent(url)}`}
target="_blank"
rel="noopener noreferrer"
>
<Eye className="h-4 w-4" />
<span className="hidden sm:inline">View PDF</span>
</Link>
</Button>
</div>
</div>
{/* PDF Controls */}
{!isLoading && pageCount > 0 && (
<div className="flex flex-wrap items-center justify-between gap-2 rounded-lg border bg-background p-3">
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={prevPage}
disabled={pageNum <= 1}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<span className="text-sm text-muted-foreground">
{pageCount > 0 ? `${pageNum} / ${pageCount}` : "Loading..."}
</span>
<Button
variant="outline"
size="sm"
onClick={nextPage}
disabled={pageNum >= pageCount}
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={zoomOut}
disabled={scale <= 0.5}
>
<ZoomOut className="h-4 w-4" />
</Button>
<span className="text-sm text-muted-foreground">
{Math.round(scale * 100)}%
</span>
<Button
variant="outline"
size="sm"
onClick={zoomIn}
disabled={scale >= 3}
>
<ZoomIn className="h-4 w-4" />
</Button>
<Button variant="outline" size="sm" onClick={rotate}>
<RotateCw className="h-4 w-4" />
</Button>
</div>
</div>
)}
{/* PDF Canvas */}
<div
className="relative overflow-hidden rounded-lg border bg-background"
tabIndex={0}
>
{isLoading && (
<>
<div className="bg-background/80 absolute inset-0 z-10 flex items-center justify-center backdrop-blur-sm">
<div className="flex flex-col items-center gap-2">
<Loader2 className="h-6 w-6 animate-spin text-primary" />
<p className="text-sm text-muted-foreground">
Loading {title.toLowerCase()}...
</p>
</div>
</div>
{/* Loading Skeleton */}
<div className="flex justify-center p-4">
<div className="h-[600px] w-full max-w-2xl animate-pulse rounded-lg bg-muted lg:h-[800px]" />
</div>
</>
)}
<div className="flex justify-center overflow-auto p-4">
<canvas
ref={canvasRef}
className="h-auto w-full max-w-4xl shadow-lg"
style={{
display: isLoading ? "none" : "block",
maxWidth: "100%",
}}
/>
</div>
</div>
{/* Mobile Notice */}
<div className="block rounded-lg border border-amber-200 bg-amber-50 p-4 dark:border-amber-800 dark:bg-amber-950/50 md:hidden">
<div className="flex items-start gap-2">
<AlertCircle className="h-5 w-5 text-amber-600 dark:text-amber-400" />
<div>
<p className="text-sm font-medium text-amber-800 dark:text-amber-200">
Better on larger screens
</p>
<p className="text-xs text-amber-700 dark:text-amber-300">
For the best viewing experience, try opening this on a tablet or
desktop. You can also download the PDF directly.
</p>
</div>
</div>
</div>
</div>
);
}
export default function CVPage() {
const [activeTab, setActiveTab] = useState('cv');
const [activeTab, setActiveTab] = useState("cv");
return (
<div className="space-y-6">
<section className="prose prose-zinc dark:prose-invert max-w-none">
<h1 className="text-2xl font-bold">Curriculum Vitae 📄</h1>
<p className="text-lg text-muted-foreground mt-2">
My academic and professional experience in computer science, robotics, and engineering.
<div className="flex items-start gap-3">
<FileText className="h-8 w-8 text-primary" />
<div>
<h1 className="mb-2 text-2xl font-bold">Curriculum Vitae</h1>
</div>
</div>
<p className="mt-2 text-lg text-muted-foreground">
My academic and professional experience in computer science, robotics,
and engineering.
</p>
</section>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList>
<TabsTrigger value="cv">CV</TabsTrigger>
<TabsTrigger value="resume">Resume</TabsTrigger>
<Tabs
value={activeTab}
onValueChange={setActiveTab}
className="space-y-6"
>
<TabsList className="grid w-fit grid-cols-2">
<TabsTrigger value="cv" className="gap-2">
<FileText className="h-4 w-4" />
Academic CV
</TabsTrigger>
<TabsTrigger value="resume" className="gap-2">
<FileText className="h-4 w-4" />
Resume
</TabsTrigger>
</TabsList>
<TabsContent value="cv">
<div className="bg-background shadow-sm rounded-lg overflow-hidden">
<iframe
src={`https://docs.google.com/viewer?url=${encodeURIComponent(CV_URL)}&embedded=true`}
width="100%"
height="600"
style={{ border: 'none' }}
className="w-full h-[calc(100vh-21rem)] lg:h-[calc(100vh-18rem)]"
>
<Card>
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle>PDF Preview Not Supported</CardTitle>
</div>
<CardDescription className="text-base">
Your browser doesn't support PDF preview.
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2 mt-2">
<Link
href={CV_URL}
target="_blank"
rel="noopener noreferrer"
>
<Badge variant="secondary" className="capitalize">
<Download className="h-4 w-4" />
Download CV
</Badge>
</Link>
</div>
</CardContent>
</Card>
</iframe>
</div>
<TabsContent value="cv" className="space-y-0">
<PDFViewer url={CV_URL} title="Academic CV" type="cv" />
</TabsContent>
<TabsContent value="resume">
<div className="bg-background shadow-sm rounded-lg overflow-hidden">
<iframe
src={`https://docs.google.com/viewer?url=${encodeURIComponent(RESUME_URL)}&embedded=true`}
width="100%"
height="600"
style={{ border: 'none' }}
className="w-full h-[calc(100vh-21rem)] lg:h-[calc(100vh-18rem)]"
>
<Card>
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle>PDF Preview Not Supported</CardTitle>
</div>
<CardDescription className="text-base">
Your browser doesn't support PDF preview.
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2 mt-2">
<Link
href={RESUME_URL}
target="_blank"
rel="noopener noreferrer"
>
<Badge variant="secondary" className="capitalize">
<Download className="h-4 w-4" />
Download Resume
</Badge>
</Link>
</div>
</CardContent>
</Card>
</iframe>
</div>
<TabsContent value="resume" className="space-y-0">
<PDFViewer url={RESUME_URL} title="Resume" type="resume" />
</TabsContent>
</Tabs>
{/* Last Updated */}
<div className="text-center text-xs text-muted-foreground">
Last updated: {new Date().toLocaleDateString()}
</div>
</div>
);
}

View File

@@ -1,4 +1,14 @@
import { ArrowUpRight, Code, FlaskConical, Users, Star } from "lucide-react";
import {
ArrowUpRight,
Code,
FlaskConical,
Users,
GraduationCap,
Building,
MapPin,
Mail,
ExternalLink,
} from "lucide-react";
import Link from "next/link";
import {
Card,
@@ -7,123 +17,333 @@ import {
CardHeader,
CardTitle,
} from "~/components/ui/card";
import { projects, name } from "~/lib/data";
import { Button } from "~/components/ui/button";
export default function HomePage() {
return (
<div className="space-y-12">
{/* About Section */}
{/* Hero Section */}
<section className="space-y-6">
<div>
<h1 className="flex items-center gap-2 text-2xl font-bold">
Hi! I'm {name[0]?.first}.
</h1>
<p className="mt-2 text-lg text-muted-foreground">
I am a Computer Science and Engineering student at Bucknell
University, passionate about robotics, software development, and
human-computer interaction. With a strong foundation in both
academic research and practical development, I bridge the gap
between theoretical concepts and real-world applications.
<div className="space-y-4">
<h1 className="text-3xl font-bold">Sean O&apos;Connor</h1>
<p className="text-xl text-muted-foreground">
Computer Science and Engineering student with experience in software
development, robotics research, and technical leadership.
</p>
<div className="flex flex-wrap gap-4 text-sm text-muted-foreground">
<div className="flex items-center gap-1">
<Mail className="h-4 w-4" />
<a href="mailto:sean@soconnor.dev" className="hover:text-primary">
sean@soconnor.dev
</a>
</div>
<div className="flex items-center gap-1">
<GraduationCap className="h-4 w-4" />
Bucknell University
</div>
<div className="flex items-center gap-1">
<MapPin className="h-4 w-4" />
Lewisburg, PA
</div>
</div>
<div className="flex gap-3">
<Button asChild>
<Link href="/cv">
View CV
<ExternalLink className="ml-2 h-4 w-4" />
</Link>
</Button>
<Button variant="outline" asChild>
<Link href="/publications">Publications</Link>
</Button>
</div>
</div>
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<Code className="h-5 w-5" />
<CardTitle>Technical Expertise</CardTitle>
</div>
</CardHeader>
<CardContent className="space-y-2">
<ul className="list-disc space-y-2 pl-5 text-muted-foreground">
<li>
Full-stack development with modern frameworks (React, Next.js,
Node.js)
</li>
<li>Robotics development using ROS2 and C++</li>
<li>Systems programming and architecture design</li>
<li>Database design and optimization (SQL, PostgreSQL)</li>
<li>Cloud infrastructure and DevOps (AWS, Docker)</li>
</ul>
</CardContent>
</Card>
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<FlaskConical className="h-5 w-5" />
<CardTitle>Research Focus</CardTitle>
</div>
</CardHeader>
<CardContent className="space-y-2">
<ul className="list-disc space-y-2 pl-5 text-muted-foreground">
<li>Human-Robot Interaction studies and experimental design</li>
<li>Published researcher at IEEE RO-MAN 2024</li>
<li>Development of experimental platforms for HRI research</li>
<li>Integration of robotics in chemical engineering research</li>
</ul>
</CardContent>
</Card>
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<Users className="h-5 w-5" />
<CardTitle>Leadership</CardTitle>
</div>
</CardHeader>
<CardContent className="space-y-2">
<ul className="list-disc space-y-2 pl-5 text-muted-foreground">
<li>President of AIChE Chem-E-Car Competition Team</li>
<li>Treasurer of Bucknell Coffee Society</li>
<li>Teaching Assistant for Computer Science courses</li>
<li>Founding member of RoboLab@Bucknell</li>
</ul>
</CardContent>
</Card>
</section>
{/* Featured Projects Section */}
{/* Current Focus */}
<section className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="flex items-center gap-2 text-2xl font-bold">
<Star className="h-6 w-6" />
Featured Projects
</h2>
<Link
href="/projects"
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
>
View all projects
<ArrowUpRight className="h-4 w-4" />
</Link>
<h2 className="text-2xl font-bold">Current Focus</h2>
<div className="grid gap-6 md:grid-cols-2">
<Card>
<CardHeader className="pb-3">
<div className="flex items-center gap-2">
<FlaskConical className="h-5 w-5" />
<CardTitle className="mb-1">
Human-Robot Interaction Research
</CardTitle>
</div>
</CardHeader>
<CardContent className="pt-0">
<p className="text-muted-foreground">
Developing a web-based platform for HRI experiments that
addresses reproducibility challenges in Wizard-of-Oz studies.
Published at IEEE RO-MAN 2024 with second publication
forthcoming.
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<div className="flex items-center gap-2">
<GraduationCap className="h-5 w-5" />
<CardTitle className="mb-1">Academic Excellence</CardTitle>
</div>
</CardHeader>
<CardContent className="pt-0">
<p className="text-muted-foreground">
Bachelor of Science in Computer Science and Engineering at
Bucknell University. 3.86 Engineering GPA, Dean&apos;s List
multiple semesters. Expected graduation: May 2026.
</p>
</CardContent>
</Card>
</div>
</section>
{/* Experience Highlights */}
<section className="space-y-6">
<h2 className="text-2xl font-bold">Experience Highlights</h2>
<div className="space-y-6">
{projects
.filter((project) => project.featured)
.slice(0, 2)
.map((project, index) => (
<Card key={index}>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>{project.title}</CardTitle>
{project.link && (
<Link
href={project.link}
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-primary"
>
<ArrowUpRight className="h-5 w-5" />
</Link>
)}
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div>
<CardTitle className="mb-1">
Software Developer - Riverhead Raceway
</CardTitle>
<CardDescription>Oct 2020 Present</CardDescription>
</div>
<Building className="h-5 w-5 text-muted-foreground" />
</div>
</CardHeader>
<CardContent className="pt-0">
<p className="text-muted-foreground">
Transformed organizational culture by building trust in
data-driven decision making. Revolutionized fan engagement
through a real-time statistics platform serving 1500+ concurrent
users. Modernized entire technical infrastructure through
containerization and automated systems.
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div>
<CardTitle className="mb-1">
Computer Science Researcher - Bucknell University
</CardTitle>
<CardDescription>Jan 2023 Present</CardDescription>
</div>
<FlaskConical className="h-5 w-5 text-muted-foreground" />
</div>
</CardHeader>
<CardContent className="pt-0">
<p className="text-muted-foreground">
Led research and authored first-author paper presented at
international conference. Built framework that enables
researchers to conduct experiments across different robot
platforms without specialized programming knowledge.
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div>
<CardTitle className="mb-1">
Teaching Assistant - Computer Science
</CardTitle>
<CardDescription>Jan 2024 Present</CardDescription>
</div>
<Users className="h-5 w-5 text-muted-foreground" />
</div>
</CardHeader>
<CardContent className="pt-0">
<p className="text-muted-foreground">
Mentored 150+ students in software engineering principles,
connecting theoretical concepts to real-world applications.
Developed learning environments that embrace productive failure.
</p>
</CardContent>
</Card>
</div>
</section>
{/* Technical Skills */}
<section className="space-y-6">
<h2 className="flex items-center gap-2 text-2xl font-bold">
<Code className="h-6 w-6" />
Technical Skills
</h2>
<div className="grid gap-6 md:grid-cols-2">
<Card>
<CardHeader className="pb-3">
<CardTitle className="mb-1">Languages & Frameworks</CardTitle>
</CardHeader>
<CardContent className="pt-0">
<p className="text-muted-foreground">
Java, C/C++, Python, JavaScript/TypeScript, React, Next.js, PHP,
SQL
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="mb-1">Backend & DevOps</CardTitle>
</CardHeader>
<CardContent className="pt-0">
<p className="text-muted-foreground">
REST APIs, MySQL, PostgreSQL, Docker, Apache Web Server, NGINX,
ROS2
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="mb-1">Cloud & Infrastructure</CardTitle>
</CardHeader>
<CardContent className="pt-0">
<p className="text-muted-foreground">
AWS, GCP, Azure, Backblaze, Linux (RHEL/Debian), CI/CD
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="mb-1">Development Tools</CardTitle>
</CardHeader>
<CardContent className="pt-0">
<p className="text-muted-foreground">
Git, JetBrains Suite, VS Code, Cursor, Linux CLI
</p>
</CardContent>
</Card>
</div>
</section>
{/* Leadership & Activities */}
<section className="space-y-6">
<h2 className="flex items-center gap-2 text-2xl font-bold">
<Users className="h-6 w-6" />
Leadership & Activities
</h2>
<div className="grid gap-6 md:grid-cols-2">
<Card>
<CardHeader className="pb-3">
<CardTitle className="mb-1">
AIChE Chem-E-Car Competition Team
</CardTitle>
<CardDescription>
Former President, Electrical and Mechanical Team Lead
</CardDescription>
</CardHeader>
<CardContent className="pt-0">
<p className="text-muted-foreground">
Pioneered team&apos;s first custom hardware solution by
designing and fabricating a microcontroller-based control
system. Improved team dynamics by introducing agile development
principles and structured communication protocols.
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="mb-1">Bucknell Coffee Society</CardTitle>
<CardDescription>Treasurer</CardDescription>
</CardHeader>
<CardContent className="pt-0">
<p className="text-muted-foreground">
Co-established and launched a new campus organization, managing
financial operations and coordinating event logistics.
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="mb-1">RoboLab@Bucknell</CardTitle>
<CardDescription>Founding Member</CardDescription>
</CardHeader>
<CardContent className="pt-0">
<p className="text-muted-foreground">
Led and participated in group discussions in a new lab bridging
computer science and psychology perspectives on human-robot
interaction, working with the complexities of human-robot trust,
job replacement, and autonomy.
</p>
</CardContent>
</Card>
</div>
</section>
{/* Quick Links */}
<section className="space-y-6">
<h2 className="text-2xl font-bold">Explore More</h2>
<div className="grid gap-4 md:grid-cols-3">
<Card className="group cursor-pointer transition-colors hover:bg-accent">
<Link href="/publications" className="block p-4">
<CardContent className="p-0">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<FlaskConical className="h-6 w-6 text-primary" />
<div>
<div className="font-medium">Research Publications</div>
<div className="text-sm text-muted-foreground">
IEEE conferences and ongoing research
</div>
</div>
</div>
<CardDescription className="text-base">
{project.description}
</CardDescription>
</CardHeader>
</Card>
))}
<ArrowUpRight className="h-4 w-4 text-muted-foreground group-hover:text-primary" />
</div>
</CardContent>
</Link>
</Card>
<Card className="group cursor-pointer transition-colors hover:bg-accent">
<Link href="/projects" className="block p-4">
<CardContent className="p-0">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Code className="h-6 w-6 text-primary" />
<div>
<div className="font-medium">Projects</div>
<div className="text-sm text-muted-foreground">
Software development and research work
</div>
</div>
</div>
<ArrowUpRight className="h-4 w-4 text-muted-foreground group-hover:text-primary" />
</div>
</CardContent>
</Link>
</Card>
<Card className="group cursor-pointer transition-colors hover:bg-accent">
<Link href="/cv" className="block p-4">
<CardContent className="p-0">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<GraduationCap className="h-6 w-6 text-primary" />
<div>
<div className="font-medium">Curriculum Vitae</div>
<div className="text-sm text-muted-foreground">
Complete academic and professional record
</div>
</div>
</div>
<ArrowUpRight className="h-4 w-4 text-muted-foreground group-hover:text-primary" />
</div>
</CardContent>
</Link>
</Card>
</div>
</section>
</div>

View File

@@ -4,7 +4,8 @@ import { Badge } from "~/components/ui/badge";
export const metadata: Metadata = {
title: "Accessibility Features",
description: "An overview of the accessibility features implemented on this website to ensure inclusive user experience.",
description:
"An overview of the accessibility features implemented on this website to ensure inclusive user experience.",
};
export default function AccessibilityPage() {
@@ -12,11 +13,15 @@ export default function AccessibilityPage() {
const project = projects.find((p) => p.title === "Accessibility Features");
return (
<div className="container pt-0 pb-6">
<div className="container pb-6 pt-0">
<div className="space-y-6">
<div>
<h1 className="text-4xl font-bold tracking-tight mb-4">{project?.title}</h1>
<p className="text-lg text-muted-foreground">{project?.longDescription}</p>
<h1 className="mb-4 text-4xl font-bold tracking-tight">
{project?.title}
</h1>
<p className="text-lg text-muted-foreground">
{project?.longDescription}
</p>
<div className="mt-4 flex flex-wrap gap-2">
{project?.tags.map((tag) => (
<Badge key={tag} variant="secondary">
@@ -28,117 +33,194 @@ export default function AccessibilityPage() {
<div className="mt-8 space-y-8">
<section>
<h2 className="text-2xl font-bold tracking-tight">Why Accessibility Matters</h2>
<h2 className="text-2xl font-bold tracking-tight">
Why Accessibility Matters
</h2>
<p className="mt-4">
As a portfolio website aimed at showcasing my technical skills and projects to potential employers and collaborators,
ensuring accessibility is not just a legal or ethical requirement it's a demonstration of my professional competence and
inclusive design thinking. Here's why accessibility is particularly important for my website:
As a portfolio website aimed at showcasing my technical skills and
projects to potential employers and collaborators, ensuring
accessibility is not just a legal or ethical requirement
it&apos;s a demonstration of my professional competence and
inclusive design thinking. Here&apos;s why accessibility is
particularly important for my website:
</p>
<ul className="list-disc pl-6 space-y-2 mt-4">
<ul className="mt-4 list-disc space-y-2 pl-6">
<li>
<strong>Universal Access:</strong> A portfolio should be accessible to all potential viewers, including
hiring managers, colleagues, and collaborators who may have disabilities or situational limitations.
<strong>Universal Access:</strong> A portfolio should be
accessible to all potential viewers, including hiring managers,
colleagues, and collaborators who may have disabilities or
situational limitations.
</li>
<li>
<strong>Professional Credibility:</strong> As a computer science and engineering student, demonstrating knowledge
of accessibility standards reflects technical competence and attention to detail that employers value.
<strong>Professional Credibility:</strong> As a computer science
and engineering student, demonstrating knowledge of
accessibility standards reflects technical competence and
attention to detail that employers value.
</li>
<li>
<strong>Technical Showcase:</strong> Implementing accessibility features serves as a practical example of the technical skills being presented.
<strong>Technical Showcase:</strong> Implementing accessibility
features serves as a practical example of the technical skills
being presented.
</li>
<li>
<strong>Academic Integrity:</strong> In an academic context, ensuring that educational resources
(like the LaTeX tutorial) are accessible to all students reflects a commitment to educational equity.
<strong>Academic Integrity:</strong> In an academic context,
ensuring that educational resources (like the LaTeX tutorial)
are accessible to all students reflects a commitment to
educational equity.
</li>
</ul>
</section>
<section>
<h2 className="text-2xl font-bold tracking-tight">Accessibility Features Implemented</h2>
<div className="space-y-6 mt-4">
<h2 className="text-2xl font-bold tracking-tight">
Accessibility Features Implemented
</h2>
<div className="mt-4 space-y-6">
<div>
<h3 className="text-xl font-semibold">1. Comprehensive Image Alt Text</h3>
<h3 className="text-xl font-semibold">
1. Comprehensive Image Alt Text
</h3>
<p className="mt-2">
Every image on my website has been carefully evaluated and provided with appropriate alt text. I distinguish between:
Every image on my website has been carefully evaluated and
provided with appropriate alt text. I distinguish between:
</p>
<ul className="list-disc pl-6 my-2">
<ul className="my-2 list-disc pl-6">
<li>
<strong>Decorative images:</strong> Images that are purely decorative, such as the LaTeX tutorial thumbnail, have alt text that
indicates their decorative nature (e.g., "Decorative thumbnail showing LaTeX code and formatting example").
<strong>Decorative images:</strong> Images that are purely
decorative, such as the LaTeX tutorial thumbnail, have alt
text that indicates their decorative nature (e.g.,
&quot;Decorative thumbnail showing LaTeX code and formatting
example&quot;).
</li>
<li>
<strong>Informative images:</strong> Images that convey information, such as project screenshots, have detailed alt text
describing their content (e.g., "Screenshot of HRIStudio application showing the robot control dashboard on a laptop").
<strong>Informative images:</strong> Images that convey
information, such as project screenshots, have detailed alt
text describing their content (e.g., &quot;Screenshot of
HRIStudio application showing the robot control dashboard on
a laptop&quot;).
</li>
</ul>
<strong>Implementation details:</strong> I implemented this by adding custom "imageAlt" properties to all projects
and "alts" arrays for travel items in my data structure, ensuring consistent image descriptions throughout the site.
<strong>Implementation details:</strong> I implemented this by
adding custom &quot;imageAlt&quot; properties to all projects
and &quot;alts&quot; arrays for travel items in my data
structure, ensuring consistent image descriptions throughout the
site.
</div>
<div>
<h3 className="text-xl font-semibold">2. Accessible Video Player</h3>
<h3 className="text-xl font-semibold">
2. Accessible Video Player
</h3>
<p className="mt-2">
I developed a custom video player for the LaTeX tutorial with several accessibility features:
I developed a custom video player for the LaTeX tutorial with
several accessibility features:
</p>
<ul className="list-disc pl-6 my-2">
<ul className="my-2 list-disc pl-6">
<li>Closed captions that can be toggled on/off</li>
<li>Full transcript available with expandable details element</li>
<li>Keyboard-accessible controls for play/pause, volume, and caption toggling</li>
<li>ARIA labels for all controls to improve screen reader compatibility</li>
<li>
Full transcript available with expandable details element
</li>
<li>
Keyboard-accessible controls for play/pause, volume, and
caption toggling
</li>
<li>
ARIA labels for all controls to improve screen reader
compatibility
</li>
<li>Video poster image with appropriate alt text</li>
</ul>
<strong>Implementation details:</strong> I created a custom AccessibleVideo component that wraps the HTML5 video element
with additional accessibility features, including captions integration, keyboard navigation, and proper ARIA attributes.
<strong>Implementation details:</strong> I created a custom
AccessibleVideo component that wraps the HTML5 video element
with additional accessibility features, including captions
integration, keyboard navigation, and proper ARIA attributes.
</div>
<div>
<h3 className="text-xl font-semibold">3. Semantic HTML and ARIA</h3>
<h3 className="text-xl font-semibold">
3. Semantic HTML and ARIA
</h3>
<p className="mt-2">
My website uses semantic HTML throughout to ensure proper structure and meaning:
My website uses semantic HTML throughout to ensure proper
structure and meaning:
</p>
<ul className="list-disc pl-6 my-2">
<li>Proper heading hierarchy (h1, h2, h3) for logical document structure</li>
<li>Semantic elements like &lt;nav&gt;, &lt;main&gt;, &lt;section&gt;, and &lt;article&gt;</li>
<li>ARIA attributes for components without native semantics</li>
<ul className="my-2 list-disc pl-6">
<li>
Proper heading hierarchy (h1, h2, h3) for logical document
structure
</li>
<li>
Semantic elements like &lt;nav&gt;, &lt;main&gt;,
&lt;section&gt;, and &lt;article&gt;
</li>
<li>
ARIA attributes for components without native semantics
</li>
<li>Skip-to-content links for keyboard users</li>
</ul>
<strong>Implementation details:</strong> I audited all pages for proper heading structure and added semantic HTML elements. For
custom components like cards and badges, I ensured proper ARIA attributes were used to convey their purpose and state.
<strong>Implementation details:</strong> I audited all pages for
proper heading structure and added semantic HTML elements. For
custom components like cards and badges, I ensured proper ARIA
attributes were used to convey their purpose and state.
</div>
<div>
<h3 className="text-xl font-semibold">4. Color Contrast and Dark Mode</h3>
<h3 className="text-xl font-semibold">
4. Color Contrast and Dark Mode
</h3>
<p className="mt-2">
I carefully selected color choices on my website to ensure readability:
I carefully selected color choices on my website to ensure
readability:
</p>
<ul className="list-disc pl-6 my-2">
<li>All text meets WCAG AA contrast requirements (4.5:1 for normal text, 3:1 for large text)</li>
<li>Dark mode support for users who prefer or require reduced brightness</li>
<li>Color is never used as the sole means of conveying information</li>
<li>Interactive elements have visual focus indicators that don't rely solely on color</li>
<ul className="my-2 list-disc pl-6">
<li>
All text meets WCAG AA contrast requirements (4.5:1 for
normal text, 3:1 for large text)
</li>
<li>
Dark mode support for users who prefer or require reduced
brightness
</li>
<li>
Color is never used as the sole means of conveying
information
</li>
<li>
Interactive elements have visual focus indicators that
don&apos;t rely solely on color
</li>
</ul>
<strong>Implementation details:</strong> I used the TailwindCSS color palette with careful consideration of contrast ratios.
The dark mode implementation respects user system preferences and provides consistent contrast ratios in both modes.
<strong>Implementation details:</strong> I used the TailwindCSS
color palette with careful consideration of contrast ratios. The
dark mode implementation respects user system preferences and
provides consistent contrast ratios in both modes.
</div>
</div>
</section>
<section>
<h2 className="text-2xl font-bold tracking-tight">Platform Limitations and Workarounds</h2>
<h2 className="text-2xl font-bold tracking-tight">
Platform Limitations and Workarounds
</h2>
<p className="mt-4">
While building this website, I encountered some limitations in the Next.js framework and implemented workarounds:
While building this website, I encountered some limitations in the
Next.js framework and implemented workarounds:
</p>
<ul className="list-disc pl-6 space-y-2 mt-4">
<ul className="mt-4 list-disc space-y-2 pl-6">
<li>
<strong>Next.js Client/Server Components:</strong> Next.js divides components into client and server components,
which can create hydration issues when implementing certain interactive accessibility features. I addressed this by
creating client-side wrapper components for features requiring interactivity, such as the video player and navigation menu.
<strong>Next.js Client/Server Components:</strong> Next.js
divides components into client and server components, which can
create hydration issues when implementing certain interactive
accessibility features. I addressed this by creating client-side
wrapper components for features requiring interactivity, such as
the video player and navigation menu.
</li>
<li>
<strong>PDF Accessibility:</strong> The CV section uses PDF rendering which has inherent accessibility limitations.
As a workaround, I provide the same information in HTML format elsewhere on the site, ensuring that users who cannot
access the PDF can still view the content.
<strong>PDF Accessibility:</strong> The CV section uses PDF
rendering which has inherent accessibility limitations. As a
workaround, I provide the same information in HTML format
elsewhere on the site, ensuring that users who cannot access the
PDF can still view the content.
</li>
</ul>
</section>
@@ -146,4 +228,4 @@ export default function AccessibilityPage() {
</div>
</div>
);
}
}

View File

@@ -7,12 +7,11 @@ import {
CardTitle,
CardDescription,
CardContent,
CardFooter,
} from "~/components/ui/card";
import { Badge } from "~/components/ui/badge";
import { Button } from "~/components/ui/button";
import Link from "next/link";
import { ArrowUpRight, Play, BookOpen, Star } from "lucide-react";
import { ArrowUpRight, Play, BookOpen, Code } from "lucide-react";
import { projects } from "~/lib/data";
import Image from "next/image";
import { CardSkeleton } from "~/components/ui/skeletons";
@@ -24,104 +23,55 @@ export default function ProjectsPage() {
setLoading(false);
}, []);
const featuredProjects = projects.filter((p) => p.featured);
const otherProjects = projects.filter((p) => !p.featured);
return (
<div className="space-y-6">
<section className="prose prose-zinc dark:prose-invert max-w-none">
<h1 className="flex items-center gap-2 text-2xl font-bold">
<Star className="h-6 w-6" />
Featured Projects
</h1>
<p className="mt-2 text-lg text-muted-foreground">
A selection of my academic and professional projects, focusing on
robotics, web development, and embedded systems.
</p>
<div className="space-y-8">
{/* Header */}
<section className="space-y-4">
<div className="flex items-center gap-3">
<Code className="h-8 w-8 text-primary" />
<div>
<h1 className="text-3xl font-bold">Projects</h1>
<p className="text-lg text-muted-foreground">
A collection of my academic research, professional work, and
personal projects spanning robotics, web development, and embedded
systems.
</p>
</div>
</div>
</section>
<div className="space-y-6">
{loading ? (
<>
<CardSkeleton />
<CardSkeleton />
<CardSkeleton />
</>
) : (
projects.map((project, index) => (
<Card key={index}>
<div className="flex flex-col lg:flex-row">
<div className="flex-1">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle>{project.title}</CardTitle>
{project.link && !project.link.startsWith("/") && (
<Link
href={project.link}
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-primary"
>
<ArrowUpRight className="h-5 w-5" />
</Link>
)}
</div>
<CardDescription className="text-base">
{project.description}
</CardDescription>
</CardHeader>
<CardContent className="pt-0">
<p className="text-sm text-muted-foreground">
{project.longDescription}
</p>
<div className="mt-4 flex flex-wrap gap-2">
{project.tags.map((tag) => (
<Badge key={tag} variant="secondary">
{tag}
</Badge>
))}
</div>
</CardContent>
{project.link && project.link.startsWith("/") && (
<CardFooter className="pt-0">
<Link href={project.link}>
<Button variant="default" size="sm" className="mt-0">
{project.title === "LaTeX Introduction Tutorial" ? (
<>
<Play className="mr-2 h-4 w-4" />
Watch the LaTeX tutorial
</>
) : (
<>
<BookOpen className="mr-2 h-4 w-4" />
View project details
</>
)}
</Button>
</Link>
</CardFooter>
)}
</div>
{project.image && (
<div className="px-6 pb-6 md:px-24 lg:w-1/3 lg:px-6 lg:py-6">
<Link
href={
project.link?.startsWith("/")
? project.link
: project.link || "#"
}
>
<div className="relative aspect-[4/3] w-full overflow-hidden transition-all hover:opacity-90">
{/* Featured Projects */}
<section className="space-y-6">
<h2 className="text-2xl font-bold">Featured Work</h2>
<div className="space-y-8">
{loading ? (
<>
<CardSkeleton />
<CardSkeleton />
<CardSkeleton />
</>
) : (
featuredProjects.map((project, index) => (
<Card key={index} className="overflow-hidden">
<div className="flex flex-col lg:flex-row">
{/* Project Image */}
{project.image && (
<div className="relative lg:w-2/5">
<div className="aspect-[16/10] lg:aspect-square">
<Image
src={project.image}
alt={project.imageAlt || project.title}
width={400}
height={300}
width={600}
height={400}
className="h-full w-full object-cover"
priority={index === 0}
/>
{project.title === "LaTeX Introduction Tutorial" && (
<div className="absolute inset-0 flex items-center justify-center bg-black/30">
<div className="bg-white/80 p-3">
<div className="absolute inset-0 flex items-center justify-center bg-black/20">
<div className="flex h-16 w-16 items-center justify-center bg-white/90 transition-transform hover:scale-110">
<Play
className="h-8 w-8 text-primary"
fill="currentColor"
@@ -130,14 +80,215 @@ export default function ProjectsPage() {
</div>
)}
</div>
</Link>
</div>
)}
{/* Project Content */}
<div className="flex flex-1 flex-col justify-between p-6">
<div className="space-y-4">
<div className="flex items-start justify-between">
<div>
<CardTitle className="text-xl">
{project.title}
</CardTitle>
<CardDescription className="mt-2 text-base">
{project.description}
</CardDescription>
</div>
{project.link && !project.link.startsWith("/") && (
<Link
href={project.link}
target="_blank"
rel="noopener noreferrer"
className="ml-4 text-muted-foreground transition-colors hover:text-primary"
>
<ArrowUpRight className="h-5 w-5" />
</Link>
)}
</div>
<p className="text-muted-foreground">
{project.longDescription}
</p>
<div className="flex flex-wrap gap-2">
{project.tags.map((tag) => (
<Badge key={tag} variant="secondary">
{tag}
</Badge>
))}
</div>
</div>
{/* Action Buttons */}
<div className="mt-6 flex flex-wrap gap-3">
{project.link && project.link.startsWith("/") && (
<Button asChild>
<Link href={project.link}>
{project.title === "LaTeX Introduction Tutorial" ? (
<>
<Play className="mr-2 h-4 w-4" />
Watch Tutorial
</>
) : (
<>
<BookOpen className="mr-2 h-4 w-4" />
Learn More
</>
)}
</Link>
</Button>
)}
{project.link && !project.link.startsWith("/") && (
<Button variant="outline" asChild>
<Link
href={project.link}
target="_blank"
rel="noopener noreferrer"
>
<ArrowUpRight className="mr-2 h-4 w-4" />
View Project
</Link>
</Button>
)}
</div>
</div>
)}
</div>
</Card>
))
)}
</div>
</section>
{/* Other Projects */}
{otherProjects.length > 0 && (
<section className="space-y-6">
<h2 className="text-2xl font-bold">Additional Projects</h2>
<div className="grid gap-6 md:grid-cols-2">
{loading ? (
<>
<CardSkeleton />
<CardSkeleton />
</>
) : (
otherProjects.map((project, index) => (
<Card key={index} className="flex flex-col">
{project.image && (
<div className="relative aspect-[16/10] overflow-hidden">
<Image
src={project.image}
alt={project.imageAlt || project.title}
width={400}
height={250}
className="h-full w-full object-cover"
/>
</div>
)}
<div className="flex flex-1 flex-col p-6">
<CardHeader className="p-0">
<div className="flex items-start justify-between">
<CardTitle className="text-lg">
{project.title}
</CardTitle>
{project.link && !project.link.startsWith("/") && (
<Link
href={project.link}
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground transition-colors hover:text-primary"
>
<ArrowUpRight className="h-4 w-4" />
</Link>
)}
</div>
<CardDescription className="mt-2">
{project.description}
</CardDescription>
</CardHeader>
<CardContent className="flex-1 p-0 pt-4">
<div className="flex flex-wrap gap-2">
{project.tags.map((tag) => (
<Badge
key={tag}
variant="secondary"
className="text-xs"
>
{tag}
</Badge>
))}
</div>
</CardContent>
{project.link && (
<div className="mt-4">
<Button
size="sm"
variant="outline"
asChild
className="w-full"
>
<Link
href={project.link}
{...(!project.link.startsWith("/") && {
target: "_blank",
rel: "noopener noreferrer",
})}
>
{project.link.startsWith("/") ? (
<>
<BookOpen className="mr-2 h-4 w-4" />
Learn More
</>
) : (
<>
<ArrowUpRight className="mr-2 h-4 w-4" />
View Project
</>
)}
</Link>
</Button>
</div>
)}
</div>
</Card>
))
)}
</div>
</section>
)}
{/* Project Stats */}
<section className="border-t pt-8">
<div className="grid gap-4 md:grid-cols-3">
<Card className="text-center">
<CardContent className="pt-6">
<div className="text-2xl font-bold">{projects.length}</div>
<div className="text-sm text-muted-foreground">
Total Projects
</div>
</Card>
))
)}
</div>
</CardContent>
</Card>
<Card className="text-center">
<CardContent className="pt-6">
<div className="text-2xl font-bold">
{[...new Set(projects.flatMap((p) => p.tags))].length}
</div>
<div className="text-sm text-muted-foreground">Technologies</div>
</CardContent>
</Card>
<Card className="text-center">
<CardContent className="pt-6">
<div className="text-2xl font-bold">
{projects.filter((p) => p.featured).length}
</div>
<div className="text-sm text-muted-foreground">Featured</div>
</CardContent>
</Card>
</div>
</section>
</div>
);
}

View File

@@ -1,10 +1,22 @@
'use client';
"use client";
import { ArrowUpRight, BookOpenText, FileText, Presentation } from "lucide-react";
import {
ArrowUpRight,
BookOpenText,
FileText,
Presentation,
BookOpen,
} from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { Badge } from "~/components/ui/badge";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import { Skeleton } from "~/components/ui/skeleton";
import { CardSkeleton } from "~/components/ui/skeletons";
import type { Publication } from "~/lib/bibtex";
@@ -13,12 +25,12 @@ import { parseBibtex } from "~/lib/bibtex";
export default function PublicationsPage() {
const [publications, setPublications] = useState<Publication[]>([]);
const [loading, setLoading] = useState(true);
const tagsToStrip = ['paperUrl', 'posterUrl'];
const tagsToStrip = ["paperUrl", "posterUrl"];
useEffect(() => {
fetch('/publications.bib')
.then(res => res.text())
.then(text => {
fetch("/publications.bib")
.then((res) => res.text())
.then((text) => {
const pubs = parseBibtex(text);
setPublications(pubs);
setLoading(false);
@@ -26,16 +38,26 @@ export default function PublicationsPage() {
}, []);
const downloadBibtex = (pub: Publication) => {
const { title, authors, venue, year, doi, abstract, type, citationType, citationKey } = pub;
const {
title,
authors,
venue,
year,
doi,
abstract,
type,
citationType,
citationKey,
} = pub;
let bibtexEntry = `@${citationType}{${citationKey},\n`;
bibtexEntry += ` title = {${title}},\n`;
bibtexEntry += ` author = {${authors.join(' and ')}},\n`;
bibtexEntry += ` author = {${authors.join(" and ")}},\n`;
bibtexEntry += ` year = {${year}},\n`;
if (type === 'conference' || type === 'workshop') {
if (type === "conference" || type === "workshop") {
bibtexEntry += ` organization = {${venue}},\n`;
} else if (type === 'journal') {
} else if (type === "journal") {
bibtexEntry += ` journal = {${venue}},\n`;
} else if (type === 'thesis') {
} else if (type === "thesis") {
bibtexEntry += ` school = {${venue}},\n`;
}
if (doi) {
@@ -46,9 +68,9 @@ export default function PublicationsPage() {
}
bibtexEntry += `}\n`;
const blob = new Blob([bibtexEntry], { type: 'text/plain' });
const blob = new Blob([bibtexEntry], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
const a = document.createElement("a");
a.href = url;
a.download = `${citationKey}.bib`;
document.body.appendChild(a);
@@ -60,8 +82,11 @@ export default function PublicationsPage() {
return (
<div className="space-y-6">
<section className="prose prose-zinc dark:prose-invert max-w-none">
<h1 className="text-2xl font-bold">Peer-Reviewed Publications 📚</h1>
<p className="text-lg text-muted-foreground mt-2">
<h1 className="flex items-center gap-2 text-2xl font-bold">
<BookOpen className="h-6 w-6" />
Peer-Reviewed Publications
</h1>
<p className="mt-2 text-lg text-muted-foreground">
My research publications in human-robot interaction and robotics.
</p>
</section>
@@ -80,7 +105,7 @@ export default function PublicationsPage() {
<div className="flex items-center justify-between">
<CardTitle>{pub.title}</CardTitle>
{pub.paperUrl && (
<Link
<Link
href={pub.paperUrl}
target="_blank"
rel="noopener noreferrer"
@@ -91,11 +116,23 @@ export default function PublicationsPage() {
)}
</div>
<CardDescription className="text-base">
{pub.authors.join(', ')}
{pub.authors.join(", ")}
</CardDescription>
<CardDescription className="text-sm">
{pub.venue} ({pub.year})
</CardDescription>
{pub.address && (
<CardDescription className="text-sm text-muted-foreground">
{pub.address}
</CardDescription>
)}
{pub.notes && (
<div className="mt-1">
<Badge variant="outline" className="text-xs">
{pub.notes}
</Badge>
</div>
)}
</CardHeader>
<CardContent className="pt-0">
{pub.abstract && (
@@ -103,52 +140,52 @@ export default function PublicationsPage() {
{pub.abstract}
</p>
)}
<div className="flex flex-wrap gap-2 mt-4">
<div className="mt-4 flex flex-wrap gap-2">
<Badge variant="secondary" className="capitalize">
{pub.type}
</Badge>
{pub.doi && (
<Link
<Link
href={`https://doi.org/${pub.doi}`}
target="_blank"
rel="noopener noreferrer"
>
<Badge variant="secondary" className="capitalize">
<ArrowUpRight className="h-4 w-4" />
<ArrowUpRight className="mr-1 h-3 w-3" />
DOI
</Badge>
</Link>
)}
{pub.paperUrl && (
<Link
<Link
href={pub.paperUrl}
target="_blank"
rel="noopener noreferrer"
>
<Badge variant="secondary" className="capitalize">
<FileText className="h-4 w-4" />
<FileText className="mr-1 h-3 w-3" />
Paper
</Badge>
</Link>
)}
{pub.posterUrl && (
<Link
<Link
href={pub.posterUrl}
target="_blank"
rel="noopener noreferrer"
>
<Badge variant="secondary" className="capitalize">
<Presentation className="h-4 w-4" />
<Presentation className="mr-1 h-3 w-3" />
Poster
</Badge>
</Link>
)}
<Badge
onClick={() => downloadBibtex(pub)}
onClick={() => downloadBibtex(pub)}
className="cursor-pointer capitalize"
variant="secondary"
>
<BookOpenText className="h-4 w-4" />
<BookOpenText className="mr-1 h-3 w-3" />
BibTeX
</Badge>
</div>

View File

@@ -1,76 +1,90 @@
'use client';
"use client";
import { useEffect, useState } from "react";
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "~/components/ui/card";
import {
Card,
CardHeader,
CardTitle,
CardContent,
CardDescription,
} from "~/components/ui/card";
import { CardSkeleton } from "~/components/ui/skeletons";
import Image from "next/image";
import { Badge } from "~/components/ui/badge";
import { travel } from "~/lib/data";
export default function TripsPage() {
const [loading, setLoading] = useState(true);
const [loading, setLoading] = useState(true);
useEffect(() => {
const timer = setTimeout(() => {
setLoading(false);
}, 1000);
return () => clearTimeout(timer);
}, []);
useEffect(() => {
const timer = setTimeout(() => {
setLoading(false);
}, 1000);
return () => clearTimeout(timer);
}, []);
return (
<div className="space-y-6">
<section className="prose prose-zinc dark:prose-invert max-w-none">
<h1 className="text-2xl font-bold">My Trips & Events 🌍</h1>
<p className="text-lg text-muted-foreground mt-2">
A collection of memorable trips and events I've attended.
</p>
</section>
return (
<div className="space-y-6">
<section className="prose prose-zinc dark:prose-invert max-w-none">
<h1 className="text-2xl font-bold">My Trips & Events 🌍</h1>
<p className="mt-2 text-lg text-muted-foreground">
A collection of memorable trips and events I&apos;ve attended.
</p>
</section>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
{loading ? (
<>
<CardSkeleton />
<CardSkeleton />
<CardSkeleton />
<CardSkeleton />
<CardSkeleton />
<CardSkeleton />
</>
) : (
travel.map((trip, index) => (
<Card key={index} className="rounded-lg overflow-hidden">
<CardHeader className="p-0">
<div className="flex flex-col">
<div className="flex overflow-x-auto space-x-0">
{trip.images.map((image, imgIndex) => (
<div key={imgIndex} className="flex-shrink-0">
<Image
src={image}
alt={trip.alts && trip.alts[imgIndex] ? trip.alts[imgIndex] : `${trip.title} - image ${imgIndex + 1}`}
width={250}
height={200}
className="object-cover min-h-[200px] max-h-[200px]"
/>
</div>
))}
</div>
</div>
</CardHeader>
<CardContent>
<div className="flex flex-col justify-items-start content-between">
<CardTitle className="mt-6 mb-2">{trip.title}</CardTitle>
<CardDescription className="">{trip.description}</CardDescription>
{/* Show badges for tags */}
<div className="flex flex-wrap gap-2 mt-4">
{trip.tags.map((tag, tagIndex) => (
<Badge key={tagIndex} variant="secondary">{tag}</Badge>
))}
</div>
</div>
</CardContent>
</Card>
))
)}
</div>
</div >
);
}
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
{loading ? (
<>
<CardSkeleton />
<CardSkeleton />
<CardSkeleton />
<CardSkeleton />
<CardSkeleton />
<CardSkeleton />
</>
) : (
travel.map((trip, index) => (
<Card key={index} className="overflow-hidden rounded-lg">
<CardHeader className="p-0">
<div className="flex flex-col">
<div className="flex space-x-0 overflow-x-auto">
{trip.images.map((image, imgIndex) => (
<div key={imgIndex} className="flex-shrink-0">
<Image
src={image}
alt={
trip.alts && trip.alts[imgIndex]
? trip.alts[imgIndex]
: `${trip.title} - image ${imgIndex + 1}`
}
width={250}
height={200}
className="max-h-[200px] min-h-[200px] object-cover"
/>
</div>
))}
</div>
</div>
</CardHeader>
<CardContent>
<div className="flex flex-col content-between justify-items-start">
<CardTitle className="mb-2 mt-6">{trip.title}</CardTitle>
<CardDescription className="">
{trip.description}
</CardDescription>
{/* Show badges for tags */}
<div className="mt-4 flex flex-wrap gap-2">
{trip.tags.map((tag, tagIndex) => (
<Badge key={tagIndex} variant="secondary">
{tag}
</Badge>
))}
</div>
</div>
</CardContent>
</Card>
))
)}
</div>
</div>
);
}

View File

@@ -1,12 +1,17 @@
"use client";
import dynamic from "next/dynamic";
import { BreadcrumbSkeleton } from "~/components/ui/breadcrumb-skeleton";
// Dynamically import PageBreadcrumb with no SSR to avoid hydration issues
const PageBreadcrumb = dynamic(() => import("~/components/PageBreadcrumb").then(mod => mod.PageBreadcrumb), {
ssr: false,
});
const PageBreadcrumb = dynamic(
() => import("~/components/PageBreadcrumb").then((mod) => mod.PageBreadcrumb),
{
ssr: false,
loading: () => <BreadcrumbSkeleton />,
},
);
export function BreadcrumbWrapper() {
return <PageBreadcrumb />;
}
}

View File

@@ -9,7 +9,6 @@ import {
Newspaper,
Plane,
X,
Accessibility,
} from "lucide-react";
import { usePathname } from "next/navigation";
import Link from "next/link";
@@ -19,11 +18,6 @@ const navItems = [
{ href: "/", label: "About", icon: Home },
{ href: "/articles", label: "Articles", icon: Newspaper },
{ href: "/projects", label: "Projects", icon: FolderGit2 },
{
href: "/projects/accessibility",
label: "Accessibility",
icon: Accessibility,
},
{ href: "/publications", label: "Publications", icon: BookOpenText },
{ href: "/travel", label: "Travel", icon: Plane },
{ href: "/cv", label: "CV", icon: FileText },
@@ -43,7 +37,7 @@ export function Navigation() {
<div className="relative mx-auto max-w-screen-xl px-4 sm:px-6 lg:px-8">
<div className="flex h-16 items-center justify-between">
<Link href="/">
<span className="text-lg font-bold">Sean O'Connor</span>
<span className="text-lg font-bold">Sean O&apos;Connor</span>
</Link>
<div className="flex items-center space-x-4">
<div className="hidden lg:flex lg:justify-end lg:space-x-4">

View File

@@ -0,0 +1,24 @@
import { Skeleton } from "~/components/ui/skeleton";
export function BreadcrumbSkeleton() {
return (
<div className="mb-6">
<div className="flex items-center space-x-2 text-sm">
{/* Home breadcrumb skeleton */}
<div className="flex items-center space-x-1">
<Skeleton className="h-3.5 w-3.5" />
<Skeleton className="h-4 w-12" />
</div>
{/* Separator */}
<Skeleton className="h-4 w-4" />
{/* Current page skeleton */}
<div className="flex items-center space-x-1">
<Skeleton className="h-3.5 w-3.5" />
<Skeleton className="h-4 w-20" />
</div>
</div>
</div>
);
}

View File

@@ -1,15 +1,12 @@
import { cn } from "~/lib/utils"
import { cn } from "~/lib/utils";
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-primary/10", className)}
{...props}
/>
)
<div className={cn("bg-primary/10 animate-pulse", className)} {...props} />
);
}
export { Skeleton }
export { Skeleton };

View File

@@ -10,7 +10,9 @@ export type Publication = {
abstract?: string;
citationType?: string;
citationKey?: string;
type: 'conference' | 'journal' | 'workshop' | 'thesis';
notes?: string;
address?: string;
type: "conference" | "journal" | "workshop" | "thesis";
};
type BibTeXEntry = {
@@ -21,11 +23,11 @@ type BibTeXEntry = {
function parseAuthors(authorString: string): string[] {
return authorString
.split(' and ')
.map(author => author.trim())
.map(author => {
if (author.includes(',')) {
const [lastName, firstName] = author.split(',').map(s => s.trim());
.split(" and ")
.map((author) => author.trim())
.map((author) => {
if (author.includes(",")) {
const [lastName, firstName] = author.split(",").map((s) => s.trim());
return `${firstName} ${lastName}`;
}
return author;
@@ -42,14 +44,14 @@ function parseBibTeXEntry(entry: string): BibTeXEntry | null {
const content = entry.slice(typeMatch[0].length);
const fields: Record<string, string> = {};
let currentField = '';
let buffer = '';
let currentField = "";
let buffer = "";
// Split into lines and process each line
const lines = content.split('\n');
const lines = content.split("\n");
for (const line of lines) {
const trimmedLine = line.trim();
if (!trimmedLine || trimmedLine === '}') continue;
if (!trimmedLine || trimmedLine === "}") continue;
// Try to match a new field
const fieldMatch = trimmedLine.match(/(\w+)\s*=\s*{(.+?)},?$/);
@@ -63,7 +65,7 @@ function parseBibTeXEntry(entry: string): BibTeXEntry | null {
buffer = fieldMatch[2];
} else if (currentField) {
// Continue previous field
buffer += ' ' + trimmedLine.replace(/},$/, '');
buffer += " " + trimmedLine.replace(/},$/, "");
}
}
@@ -77,33 +79,38 @@ function parseBibTeXEntry(entry: string): BibTeXEntry | null {
export function parseBibtex(bibtex: string): Publication[] {
const entries = bibtex
.split('@')
.split("@")
.slice(1) // Skip first empty element
.map(entry => parseBibTeXEntry(entry))
.map((entry) => parseBibTeXEntry(entry))
.filter((entry): entry is BibTeXEntry => entry !== null);
return entries.map(entry => {
return entries.map((entry) => {
const publicationType = (() => {
switch (entry.type) {
case 'inproceedings':
case 'conference':
return 'conference';
case 'article':
return 'journal';
case 'mastersthesis':
return 'thesis';
case 'workshop':
return 'workshop';
case "inproceedings":
case "conference":
return "conference";
case "article":
return "journal";
case "mastersthesis":
return "thesis";
case "workshop":
return "workshop";
default:
return 'journal';
return "journal";
}
})();
return {
title: entry.fields.title?.replace(/[{}]/g, '') || '',
authors: parseAuthors(entry.fields.author || ''),
venue: entry.fields.booktitle || entry.fields.journal || entry.fields.organization || entry.fields.school || '',
year: parseInt(entry.fields.year || '0', 10),
title: entry.fields.title?.replace(/[{}]/g, "") || "",
authors: parseAuthors(entry.fields.author || ""),
venue:
entry.fields.booktitle ||
entry.fields.journal ||
entry.fields.organization ||
entry.fields.school ||
"",
year: parseInt(entry.fields.year || "0", 10),
doi: entry.fields.doi,
url: entry.fields.url,
paperUrl: entry.fields.paperurl,
@@ -111,7 +118,9 @@ export function parseBibtex(bibtex: string): Publication[] {
abstract: entry.fields.abstract,
citationType: entry.type,
citationKey: entry.citationKey,
type: publicationType
notes: entry.fields.note || entry.fields.notes,
address: entry.fields.address,
type: publicationType,
};
});
}
}

View File

@@ -50,49 +50,51 @@
--spacing: 0.25rem;
}
.dark {
--background: 0 0% 12%;
--foreground: 0 0% 98.5%;
--card: 0 0% 18%;
--card-foreground: 0 0% 98.5%;
--popover: 0 0% 26.9%;
--popover-foreground: 0 0% 98.5%;
--primary: 0 0% 55.5%;
--primary-foreground: 0 0% 98.5%;
--secondary: 0 0% 26.9%;
--secondary-foreground: 0 0% 98.5%;
--muted: 0 0% 26.9%;
--muted-foreground: 0 0% 71%;
--accent: 0 0% 37.2%;
--accent-foreground: 0 0% 98.5%;
--destructive: 7 85% 70%;
--destructive-foreground: 0 0% 26.9%;
--border: 0 0% 25%;
--input: 0 0% 43.9%;
--ring: 0 0% 55.5%;
--chart-1: 0 0% 55.5%;
--chart-2: 0 0% 55.5%;
--chart-3: 0 0% 55.5%;
--chart-4: 0 0% 55.5%;
--chart-5: 0 0% 55.5%;
@media (prefers-color-scheme: dark) {
:root {
--background: 0 0% 12%;
--foreground: 0 0% 98.5%;
--card: 0 0% 18%;
--card-foreground: 0 0% 98.5%;
--popover: 0 0% 26.9%;
--popover-foreground: 0 0% 98.5%;
--primary: 0 0% 55.5%;
--primary-foreground: 0 0% 98.5%;
--secondary: 0 0% 26.9%;
--secondary-foreground: 0 0% 98.5%;
--muted: 0 0% 26.9%;
--muted-foreground: 0 0% 71%;
--accent: 0 0% 37.2%;
--accent-foreground: 0 0% 98.5%;
--destructive: 7 85% 70%;
--destructive-foreground: 0 0% 26.9%;
--border: 0 0% 25%;
--input: 0 0% 43.9%;
--ring: 0 0% 55.5%;
--chart-1: 0 0% 55.5%;
--chart-2: 0 0% 55.5%;
--chart-3: 0 0% 55.5%;
--chart-4: 0 0% 55.5%;
--chart-5: 0 0% 55.5%;
--font-sans: Geist Mono, monospace;
--font-serif: Geist Mono, monospace;
--font-mono: Geist Mono, monospace;
--radius: 0rem;
--shadow-2xs: 0px 1px 0px 0px hsl(0 0% 0% / 0);
--shadow-xs: 0px 1px 0px 0px hsl(0 0% 0% / 0);
--shadow-sm:
0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 1px 2px -1px hsl(0 0% 0% / 0);
--shadow:
0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 1px 2px -1px hsl(0 0% 0% / 0);
--shadow-md:
0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 2px 4px -1px hsl(0 0% 0% / 0);
--shadow-lg:
0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 4px 6px -1px hsl(0 0% 0% / 0);
--shadow-xl:
0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 8px 10px -1px hsl(0 0% 0% / 0);
--shadow-2xl: 0px 1px 0px 0px hsl(0 0% 0% / 0);
--font-sans: Geist Mono, monospace;
--font-serif: Geist Mono, monospace;
--font-mono: Geist Mono, monospace;
--radius: 0rem;
--shadow-2xs: 0px 1px 0px 0px hsl(0 0% 0% / 0);
--shadow-xs: 0px 1px 0px 0px hsl(0 0% 0% / 0);
--shadow-sm:
0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 1px 2px -1px hsl(0 0% 0% / 0);
--shadow:
0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 1px 2px -1px hsl(0 0% 0% / 0);
--shadow-md:
0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 2px 4px -1px hsl(0 0% 0% / 0);
--shadow-lg:
0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 4px 6px -1px hsl(0 0% 0% / 0);
--shadow-xl:
0px 1px 0px 0px hsl(0 0% 0% / 0), 0px 8px 10px -1px hsl(0 0% 0% / 0);
--shadow-2xl: 0px 1px 0px 0px hsl(0 0% 0% / 0);
}
}
body {