mirror of
https://github.com/soconnor0919/personal-website.git
synced 2026-02-05 00:06:36 -05:00
add pubs
This commit is contained in:
@@ -2,25 +2,34 @@
|
||||
|
||||
export default function CVPage() {
|
||||
return (
|
||||
<div className="bg-white shadow-sm rounded-lg overflow-hidden">
|
||||
<object
|
||||
data="/cv.pdf"
|
||||
type="application/pdf"
|
||||
className="w-full h-[calc(100vh-11rem)]"
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center p-8">
|
||||
<p className="text-lg text-muted-foreground">
|
||||
Your browser doesn't support PDF preview.
|
||||
</p>
|
||||
<a
|
||||
href="/cv.pdf"
|
||||
download
|
||||
className="mt-4 inline-flex items-center justify-center px-4 pt-2 pb-0 border border-transparent text-sm font-medium rounded-md text-white bg-primary hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
|
||||
>
|
||||
Download PDF
|
||||
</a>
|
||||
</div>
|
||||
</object>
|
||||
<div className="space-y-8">
|
||||
<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">
|
||||
My academic and professional experience in computer science, robotics, and engineering.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div className="bg-white shadow-sm rounded-lg overflow-hidden">
|
||||
<object
|
||||
data="/cv.pdf"
|
||||
type="application/pdf"
|
||||
className="w-full h-[calc(100vh-18rem)] lg:h-[calc(100vh-15rem)]"
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center p-8">
|
||||
<p className="text-lg text-muted-foreground">
|
||||
Your browser doesn't support PDF preview.
|
||||
</p>
|
||||
<a
|
||||
href="/cv.pdf"
|
||||
download
|
||||
className="mt-4 inline-flex items-center justify-center px-4 pt-2 pb-0 border border-transparent text-sm font-medium rounded-md text-white bg-primary hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
|
||||
>
|
||||
Download PDF
|
||||
</a>
|
||||
</div>
|
||||
</object>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ export default function HomePage() {
|
||||
{/* About Section */}
|
||||
<section className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">About Me</h1>
|
||||
<h1 className="text-2xl font-bold">Hi! I'm Sean.</h1>
|
||||
<p className="text-lg text-muted-foreground mt-2">
|
||||
I'm a Computer Science and Engineering student at Bucknell University, passionate about robotics,
|
||||
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.
|
||||
</p>
|
||||
|
||||
@@ -57,8 +57,10 @@ export default function ProjectsPage() {
|
||||
<Image
|
||||
src={project.image}
|
||||
alt={project.title}
|
||||
fill
|
||||
className="object-contain"
|
||||
width={400}
|
||||
height={300}
|
||||
className="object-contain w-full h-full"
|
||||
priority={index === 0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
124
src/app/publications/page.tsx
Normal file
124
src/app/publications/page.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
'use client';
|
||||
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "~/components/ui/card";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
import { ArrowUpRight, FileText, Presentation } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { parseBibtex } from "~/lib/bibtex";
|
||||
import { useEffect, useState } from "react";
|
||||
import type { Publication } from "~/lib/bibtex";
|
||||
import { Skeleton } from "~/components/ui/skeleton";
|
||||
|
||||
export default function PublicationsPage() {
|
||||
const [publications, setPublications] = useState<Publication[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/publications.bib')
|
||||
.then(res => res.text())
|
||||
.then(text => {
|
||||
const pubs = parseBibtex(text);
|
||||
setPublications(pubs);
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<section className="prose prose-zinc dark:prose-invert max-w-none">
|
||||
<h1 className="text-2xl font-bold">Publications</h1>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
My research publications in human-robot interaction and robotics.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div className="space-y-6">
|
||||
{loading ? (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<Skeleton className="h-6 w-1/2" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-full mt-2" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
publications.map((pub, index) => (
|
||||
<Card key={index}>
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>{pub.title}</CardTitle>
|
||||
{pub.url && (
|
||||
<Link
|
||||
href={pub.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-muted-foreground hover:text-primary"
|
||||
>
|
||||
<ArrowUpRight className="h-5 w-5" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<CardDescription className="text-base">
|
||||
{pub.authors.join(', ')}
|
||||
</CardDescription>
|
||||
<CardDescription className="text-sm">
|
||||
{pub.venue} ({pub.year})
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
{pub.abstract && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{pub.abstract}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-2 mt-4">
|
||||
<Badge variant="secondary" className="capitalize">
|
||||
{pub.type}
|
||||
</Badge>
|
||||
{pub.doi && (
|
||||
<Link
|
||||
href={`https://doi.org/${pub.doi}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Badge variant="outline" className="capitalize">
|
||||
<ArrowUpRight className="h-4 w-4" />
|
||||
DOI
|
||||
</Badge>
|
||||
</Link>
|
||||
)}
|
||||
{pub.paperUrl && (
|
||||
<Link
|
||||
href={pub.paperUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Badge variant="outline" className="capitalize">
|
||||
<FileText className="h-4 w-4" />
|
||||
Paper
|
||||
</Badge>
|
||||
</Link>
|
||||
)}
|
||||
{pub.posterUrl && (
|
||||
<Link
|
||||
href={pub.posterUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Badge variant="outline" className="capitalize">
|
||||
<Presentation className="h-4 w-4" />
|
||||
Poster
|
||||
</Badge>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,12 +4,13 @@ import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { useState } from 'react';
|
||||
import { Home, FolderGit2, FileText, Menu, X } from 'lucide-react';
|
||||
import { Home, FolderGit2, FileText, BookOpenText, Menu, X } from 'lucide-react';
|
||||
|
||||
// Define the nav items without icons
|
||||
const navItems = [
|
||||
{ href: '/', label: 'About', icon: Home },
|
||||
{ href: '/projects', label: 'Projects', icon: FolderGit2 },
|
||||
{ href: '/publications', label: 'Publications', icon: BookOpenText },
|
||||
{ href: '/cv', label: 'CV', icon: FileText },
|
||||
];
|
||||
|
||||
|
||||
@@ -65,8 +65,9 @@ export function Sidebar() {
|
||||
<Image
|
||||
src="/headshot.png"
|
||||
alt="Sean O'Connor"
|
||||
fill
|
||||
className="object-cover"
|
||||
width={240}
|
||||
height={240}
|
||||
className="object-cover rounded-xl"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
@@ -119,9 +120,12 @@ export function Sidebar() {
|
||||
<Image
|
||||
src="/headshot.png"
|
||||
alt="Sean O'Connor"
|
||||
fill
|
||||
className="object-cover"
|
||||
width={240}
|
||||
height={240}
|
||||
className="object-cover rounded-xl"
|
||||
priority
|
||||
placeholder="blur"
|
||||
blurDataURL="data:image/jpeg;base64,/9j..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
"inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
15
src/components/ui/skeleton.tsx
Normal file
15
src/components/ui/skeleton.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
100
src/lib/bibtex.ts
Normal file
100
src/lib/bibtex.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
export type Publication = {
|
||||
title: string;
|
||||
authors: string[];
|
||||
venue: string;
|
||||
year: number;
|
||||
doi?: string;
|
||||
url?: string;
|
||||
paperUrl?: string;
|
||||
posterUrl?: string;
|
||||
abstract?: string;
|
||||
type: 'conference' | 'journal' | 'workshop' | 'thesis';
|
||||
};
|
||||
|
||||
type BibTeXEntry = {
|
||||
type: string;
|
||||
fields: Record<string, string>;
|
||||
};
|
||||
|
||||
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());
|
||||
return `${firstName} ${lastName}`;
|
||||
}
|
||||
return author;
|
||||
});
|
||||
}
|
||||
|
||||
function parseBibTeXEntry(entry: string): BibTeXEntry | null {
|
||||
// Match the entry type and content
|
||||
const typeMatch = entry.match(/^(\w+)\s*{\s*[\w\d-_]+\s*,/);
|
||||
if (!typeMatch) return null;
|
||||
|
||||
const type = typeMatch[1]!.toLowerCase();
|
||||
const content = entry.slice(typeMatch[0].length);
|
||||
|
||||
const fields: Record<string, string> = {};
|
||||
let currentField = '';
|
||||
let buffer = '';
|
||||
|
||||
// Split into lines and process each line
|
||||
const lines = content.split('\n');
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim();
|
||||
if (!trimmedLine || trimmedLine === '}') continue;
|
||||
|
||||
// Try to match a new field
|
||||
const fieldMatch = trimmedLine.match(/(\w+)\s*=\s*{(.+?)},?$/);
|
||||
if (fieldMatch?.[1] && fieldMatch?.[2]) {
|
||||
// Save previous field if exists
|
||||
if (currentField) {
|
||||
fields[currentField] = buffer.trim();
|
||||
}
|
||||
// Start new field
|
||||
currentField = fieldMatch[1].toLowerCase();
|
||||
buffer = fieldMatch[2];
|
||||
} else if (currentField) {
|
||||
// Continue previous field
|
||||
buffer += ' ' + trimmedLine.replace(/},$/, '');
|
||||
}
|
||||
}
|
||||
|
||||
// Save last field if exists
|
||||
if (currentField) {
|
||||
fields[currentField] = buffer.trim();
|
||||
}
|
||||
|
||||
return { type, fields };
|
||||
}
|
||||
|
||||
export function parseBibtex(bibtex: string): Publication[] {
|
||||
const entries = bibtex
|
||||
.split('@')
|
||||
.slice(1) // Skip first empty element
|
||||
.map(entry => parseBibTeXEntry(entry))
|
||||
.filter((entry): entry is BibTeXEntry => entry !== null);
|
||||
|
||||
return entries.map(entry => {
|
||||
const publicationType =
|
||||
entry.type === 'inproceedings' ? 'conference' :
|
||||
entry.type === 'article' ? 'journal' :
|
||||
entry.type === 'mastersthesis' ? 'thesis' : 'workshop';
|
||||
|
||||
return {
|
||||
title: entry.fields.title?.replace(/[{}]/g, '') || '',
|
||||
authors: parseAuthors(entry.fields.author || ''),
|
||||
venue: entry.fields.booktitle || entry.fields.journal || '',
|
||||
year: parseInt(entry.fields.year || '0', 10),
|
||||
doi: entry.fields.doi,
|
||||
url: entry.fields.url,
|
||||
paperUrl: entry.fields.paperurl,
|
||||
posterUrl: entry.fields.posterurl,
|
||||
abstract: entry.fields.abstract,
|
||||
type: publicationType
|
||||
};
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user