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

File diff suppressed because one or more lines are too long

View File

@@ -9,7 +9,7 @@ const nextConfig = {
reactStrictMode: true,
experimental: {
serverActions: {
bodySizeLimit: '2mb',
bodySizeLimit: "2mb",
},
},
webpack: (config) => {
@@ -17,32 +17,24 @@ const nextConfig = {
config.resolve.alias.encoding = false;
return config;
},
// Add this section to disable linting during build
eslint: {
ignoreDuringBuilds: true,
},
// Add this section to disable type checking during build
typescript: {
ignoreBuildErrors: true,
},
// Add this section
async rewrites() {
return [
{
source: '/content/:path*',
destination: '/api/content/:path*',
source: "/content/:path*",
destination: "/api/content/:path*",
},
];
},
images: {
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
formats: ['image/webp', 'image/avif'],
formats: ["image/webp", "image/avif"],
minimumCacheTTL: 60,
remotePatterns: [
{
protocol: 'https',
hostname: '**',
protocol: "https",
hostname: "**",
},
],
},

10504
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,12 @@
"build": "next build",
"dev": "next dev",
"lint": "next lint",
"start": "next start"
"lint:fix": "next lint --fix",
"preview": "next build && next start",
"start": "next start",
"typecheck": "tsc --noEmit",
"format:write": "prettier --write \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
"format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,mdx}\" --cache"
},
"dependencies": {
"@radix-ui/react-avatar": "^1.1.10",
@@ -24,6 +29,7 @@
"@radix-ui/react-tooltip": "^1.1.3",
"@react-pdf/renderer": "^4.1.5",
"@t3-oss/env-nextjs": "^0.11.1",
"@types/pdfjs-dist": "^2.10.377",
"@vercel/analytics": "^1.3.2",
"@vercel/speed-insights": "^1.0.14",
"class-variance-authority": "^0.7.0",
@@ -33,7 +39,7 @@
"geist": "^1.3.1",
"lucide-react": "^0.454.0",
"next": "^15.0.2",
"pdfjs-dist": "^4.9.155",
"pdfjs-dist": "^4.10.38",
"radix-ui": "^1.0.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",

File diff suppressed because one or more lines are too long

View File

@@ -1,12 +1,22 @@
@inproceedings{OConnor2025,
title = {A Web-Based Wizard-of-Oz Platform for Collaborative and Reproducible Human-Robot Interaction Research},
author = {Sean O'Connor and L. Felipe Perrone},
year = {2025},
booktitle = {34th IEEE International Conference on Robot and Human Interactive Communication},
address = {Eindhoven, The Netherlands},
abstract = {Human-robot interaction (HRI) research plays a pivotal role in shaping how robots communicate and collaborate with humans. However, conducting HRI studies can be challenging, particularly those employing the Wizard-of-Oz (WoZ) technique. WoZ user studies can have technical and methodological complexities that may render the results irreproducible. We propose to address these challenges with HRIStudio, a modular web-based platform designed to streamline the design, the execution, and the analysis of WoZ experiments. HRIStudio offers an intuitive interface for experiment creation, real-time control and monitoring during experimental runs, and comprehensive data logging and playback tools for analysis and reproducibility. By lowering technical barriers, promoting collaboration, and offering methodological guidelines, HRIStudio aims to make human-centered robotics research easier and empower researchers to develop scientifically rigorous user studies.},
note = {Accepted for publication}
}
@inproceedings{OConnor2024,
title = {HRIStudio: A Framework for Wizard-of-Oz Experiments in Human-Robot Interaction Studies (Late Breaking Report)},
author = {Sean O'Connor and L. Felipe Perrone},
year = {2024},
organization = {33rd IEEE International Conference on Robot and Human Interactive Communication},
address = {Pasadena, CA, USA},
abstract = {Human-robot interaction (HRI) research plays a pivotal role in shaping how robots communicate and collaborate with humans. However, conducting HRI studies, particularly those employing the Wizard-of-Oz (WoZ) technique, can be challenging. WoZ user studies can have complexities at the technical and methodological levels that may render the results irreproducible. We propose to address these challenges with HRIStudio, a novel web-based platform designed to streamline the design, execution, and analysis of WoZ experiments. HRIStudio offers an intuitive interface for experiment creation, real-time control and monitoring during experimental runs, and comprehensive data logging and playback tools for analysis and reproducibility. By lowering technical barriers, promoting collaboration, and offering methodological guidelines, HRIStudio aims to make human-centered robotics research easier, and at the same time, empower researchers to develop scientifically rigorous user studies.},
url = {https://soconnor.dev/publications/hristudio-lbr.pdf},
paperUrl = {/publications/hristudio-lbr.pdf},
posterUrl = {/publications/hristudio-lbr-poster.pdf}
}
posterUrl = {/publications/hristudio-lbr-poster.pdf},
note = {Late breaking report}
}

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 {

View File

@@ -2,7 +2,7 @@ import { type Config } from "tailwindcss";
import { fontFamily } from "tailwindcss/defaultTheme";
export default {
darkMode: ["class"],
darkMode: "media",
content: ["./src/**/*.tsx"],
theme: {
extend: {

View File

@@ -13,11 +13,7 @@
"noUncheckedIndexedAccess": true,
"checkJs": true,
/* Bundled projects */
"lib": [
"dom",
"dom.iterable",
"ES2022"
],
"lib": ["dom", "dom.iterable", "ES2022"],
"noEmit": true,
"module": "ESNext",
"moduleResolution": "Bundler",
@@ -31,9 +27,7 @@
/* Path Aliases */
"baseUrl": ".",
"paths": {
"~/*": [
"./src/*"
]
"~/*": ["./src/*"]
}
},
"include": [
@@ -45,7 +39,5 @@
"**/*.js",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
"exclude": ["node_modules", "drizzle.config.ts"]
}