Initial commit

This commit is contained in:
2024-10-27 11:04:40 -07:00
commit 15047541f4
39 changed files with 8203 additions and 0 deletions

14
src/app/api/cv/route.ts Normal file
View File

@@ -0,0 +1,14 @@
import { NextResponse } from 'next/server';
import { promises as fs } from 'fs';
import path from 'path';
export async function GET() {
try {
const cvPath = path.join(process.cwd(), 'cv.tex');
const cvContent = await fs.readFile(cvPath, 'utf8');
return NextResponse.json({ content: cvContent });
} catch (error) {
return NextResponse.json({ error: 'Failed to load CV' }, { status: 500 });
}
}

26
src/app/cv/page.tsx Normal file
View File

@@ -0,0 +1,26 @@
'use client';
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-8rem)]"
>
<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 py-2 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>
);
}

28
src/app/layout.tsx Normal file
View File

@@ -0,0 +1,28 @@
import { inter } from "~/lib/fonts"
import "~/styles/globals.css"
import { Navigation } from "~/components/Navigation"
import { Sidebar } from "~/components/Sidebar"
export const metadata = {
title: "Sean O'Connor",
description: "Personal website and portfolio",
icons: [{ rel: "icon", url: "/favicon.ico" }],
}
export default function RootLayout({ children }: React.PropsWithChildren) {
return (
<html lang="en" className={inter.className}>
<body className="font-sans bg-background text-foreground min-h-screen">
<Navigation />
<div className="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex flex-col lg:flex-row lg:gap-12 py-8">
<Sidebar />
<main className="flex-1">
{children}
</main>
</div>
</div>
</body>
</html>
)
}

114
src/app/page.tsx Normal file
View File

@@ -0,0 +1,114 @@
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "~/components/ui/card";
import { Badge } from "~/components/ui/badge";
import Link from "next/link";
import { ArrowUpRight, Code, FlaskConical, Users } from 'lucide-react';
import { projects } from "~/lib/data";
export default function HomePage() {
return (
<div className="space-y-12">
{/* About Section */}
<section className="space-y-6">
<div>
<h1 className="text-2xl font-bold">About Me</h1>
<p className="text-lg text-muted-foreground mt-2">
I'm 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>
</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 pl-5 space-y-2">
<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 pl-5 space-y-2">
<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 pl-5 space-y-2">
<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 */}
<section className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold">Featured Projects</h2>
<Link
href="/projects"
className="text-sm text-muted-foreground hover:text-primary flex items-center gap-1"
>
View all projects
<ArrowUpRight className="h-4 w-4" />
</Link>
</div>
<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>
)}
</div>
<CardDescription className="text-base">{project.description}</CardDescription>
</CardHeader>
</Card>
))}
</div>
</section>
</div>
);
}

72
src/app/projects/page.tsx Normal file
View File

@@ -0,0 +1,72 @@
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "~/components/ui/card";
import { Badge } from "~/components/ui/badge";
import Link from "next/link";
import { ArrowUpRight } from "lucide-react";
import { projects } from "~/lib/data";
import Image from "next/image";
export default function ProjectsPage() {
return (
<div className="space-y-8">
<section className="prose prose-zinc dark:prose-invert max-w-none">
<h1 className="text-2xl font-bold">Featured Projects</h1>
<p className="text-lg text-muted-foreground">
A selection of my academic and professional projects, focusing on robotics,
web development, and embedded systems.
</p>
</section>
<div className="space-y-6">
{projects.map((project, index) => (
<Card key={index}>
<div className="flex flex-col lg:flex-row lg:items-center">
<div className="flex-1">
<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>
)}
</div>
<CardDescription className="text-base">{project.description}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm 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>
</CardContent>
</div>
{project.image && (
<div className="px-6 pb-6 lg:py-6 lg:w-1/3">
<div className="relative aspect-[4/3] w-full overflow-hidden rounded-lg">
<Image
src={project.image}
alt={project.title}
fill
className="object-contain"
/>
</div>
</div>
)}
</div>
</Card>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,43 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { cn } from "~/lib/utils";
const navItems = [
{ href: '/', label: 'About' },
{ href: '/projects', label: 'Projects' },
{ href: '/cv', label: 'CV' },
];
export function Navigation() {
const pathname = usePathname();
return (
<nav className="border-b">
<div className="max-w-screen-xl mx-auto 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>
</Link>
<div className="flex gap-8">
{navItems.map((item) => (
<Link
key={item.href}
href={item.href}
className={cn(
"text-sm font-medium transition-colors hover:text-primary",
pathname === item.href
? "text-primary"
: "text-muted-foreground"
)}
>
{item.label}
</Link>
))}
</div>
</div>
</div>
</nav>
);
}

109
src/components/Sidebar.tsx Normal file
View File

@@ -0,0 +1,109 @@
'use client';
import Image from 'next/image';
import { Mail, Phone, Globe, School, Linkedin } from 'lucide-react';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
import { useEffect, useState } from 'react';
export function Sidebar() {
const pathname = usePathname();
const isHomePage = pathname === '/';
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
// Initial check
const checkMobile = () => {
setIsMobile(window.innerWidth < 1024);
};
// Set initial state
checkMobile();
// Add resize listener
window.addEventListener('resize', checkMobile);
// Cleanup
return () => window.removeEventListener('resize', checkMobile);
}, []);
// If not homepage and on mobile, don't render
if (!isHomePage && isMobile) {
return null;
}
return (
<div className="w-full lg:w-64 p-6 space-y-6">
{/* Container with max-width on mobile */}
<div className="max-w-[240px] mx-auto lg:max-w-none">
<div className="aspect-square relative overflow-hidden rounded-xl w-full">
<Image
src="/headshot.png"
alt="Sean O'Connor"
fill
className="object-cover"
priority
/>
</div>
</div>
<div className="text-center lg:text-left space-y-2">
<h2 className="font-bold text-xl hover:text-primary transition-colors">Sean O'Connor</h2>
<p className="text-sm text-muted-foreground flex items-center gap-2 justify-center lg:justify-start">
Computer Science and Engineering
</p>
<p className="text-sm text-muted-foreground flex items-center gap-2 justify-center lg:justify-start">
<School className="h-4 w-4" />
Bucknell University
</p>
</div>
<div className="space-y-3 text-sm">
<div>
<h3 className="text-xs uppercase text-muted-foreground font-medium mb-2 text-center lg:text-left">Contact</h3>
<div className="space-y-2">
<a
href="mailto:sean@soconnor.dev"
className="flex items-center gap-2 text-muted-foreground hover:text-primary transition-colors justify-center lg:justify-start"
>
<Mail className="h-4 w-4" />
<span>Personal Email</span>
</a>
<a
href="mailto:sso005@bucknell.edu"
className="flex items-center gap-2 text-muted-foreground hover:text-primary transition-colors justify-center lg:justify-start"
>
<Mail className="h-4 w-4" />
<span>University Email</span>
</a>
<a
href="tel:+16316016555"
className="flex items-center gap-2 text-muted-foreground hover:text-primary transition-colors justify-center lg:justify-start"
>
<Phone className="h-4 w-4" />
<span>Phone</span>
</a>
<a
href="https://soconnor.dev"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 text-muted-foreground hover:text-primary transition-colors justify-center lg:justify-start"
>
<Globe className="h-4 w-4" />
<span>Website</span>
</a>
<a
href="https://www.linkedin.com/in/bu-soconnor"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 text-muted-foreground hover:text-primary transition-colors justify-center lg:justify-start"
>
<Linkedin className="h-4 w-4" />
<span>LinkedIn</span>
</a>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,50 @@
"use client"
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "~/lib/utils"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

View File

@@ -0,0 +1,36 @@
import * as React from "react"
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",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View File

@@ -0,0 +1,57 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "~/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@@ -0,0 +1,76 @@
import * as React from "react"
import { cn } from "~/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@@ -0,0 +1,205 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import {
CheckIcon,
ChevronRightIcon,
DotFilledIcon,
} from "@radix-ui/react-icons"
import { cn } from "~/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<DotFilledIcon className="h-4 w-4 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

44
src/env.js Normal file
View File

@@ -0,0 +1,44 @@
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
export const env = createEnv({
/**
* Specify your server-side environment variables schema here. This way you can ensure the app
* isn't built with invalid env vars.
*/
server: {
DATABASE_URL: z.string().url(),
NODE_ENV: z
.enum(["development", "test", "production"])
.default("development"),
},
/**
* Specify your client-side environment variables schema here. This way you can ensure the app
* isn't built with invalid env vars. To expose them to the client, prefix them with
* `NEXT_PUBLIC_`.
*/
client: {
// NEXT_PUBLIC_CLIENTVAR: z.string(),
},
/**
* You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
* middlewares) or client-side so we need to destruct manually.
*/
runtimeEnv: {
DATABASE_URL: process.env.DATABASE_URL,
NODE_ENV: process.env.NODE_ENV,
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
},
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
* useful for Docker builds.
*/
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
/**
* Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and
* `SOME_VAR=''` will throw an error.
*/
emptyStringAsUndefined: true,
});

36
src/lib/data.ts Normal file
View File

@@ -0,0 +1,36 @@
export const projects = [
{
title: "HRIStudio",
description: "A modular web-based experimental platform for human-robot interaction studies using the Wizard of Oz experimental paradigm.",
longDescription: "Engineered a comprehensive platform that enables researchers to conduct human-robot interaction experiments without requiring extensive programming knowledge. The system integrates with ROS2 and provides a user-friendly interface for experiment design and execution.",
tags: ["ROS2", "React", "TypeScript", "C++", "Python"],
link: "https://github.com/soconnor0919/hristudio",
image: "/hristudio_laptop.png",
featured: true
},
{
title: "Personal Website",
description: "Modern, responsive personal website built with Next.js and TailwindCSS.",
longDescription: "Designed and developed a personal portfolio website using modern web technologies. Features include responsive design, dark mode support, PDF rendering for CV display, and a clean, professional interface for showcasing projects and experience.",
tags: ["Next.js", "TypeScript", "TailwindCSS", "React"],
link: "https://github.com/soconnor0919/personal-website",
featured: true
},
{
title: "Race Statistics Platform",
description: "High-performance race statistics platform serving real-time data to 1500+ concurrent users.",
longDescription: "Developed and deployed a complete race management system that handles registration, live timing, and results distribution. The platform replaced manual processes with digital solutions, significantly improving efficiency and user experience.",
tags: ["Next.js", "PostgreSQL", "WebSockets", "Docker"],
link: "https://riverheadraceway.com",
featured: true
},
{
title: "Chem-E-Car Control System",
description: "Custom microcontroller-based control system for hydrogen fuel cell regulation.",
longDescription: "Pioneered the team's first custom hardware solution, implementing a finite state machine architecture that integrates spectrometer readings, relay control, and LED feedback for real-time reaction monitoring.",
tags: ["C++", "Embedded Systems", "Hardware Design"],
link: "https://github.com/soconnor0919/national_fa24",
featured: true
},
];

6
src/lib/fonts.ts Normal file
View File

@@ -0,0 +1,6 @@
import { Inter } from 'next/font/google'
export const inter = Inter({
subsets: ['latin'],
display: 'swap',
});

6
src/lib/utils.ts Normal file
View File

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

18
src/server/db/index.ts Normal file
View File

@@ -0,0 +1,18 @@
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import { env } from "~/env";
import * as schema from "./schema";
/**
* Cache the database connection in development. This avoids creating a new connection on every HMR
* update.
*/
const globalForDb = globalThis as unknown as {
conn: postgres.Sql | undefined;
};
const conn = globalForDb.conn ?? postgres(env.DATABASE_URL);
if (env.NODE_ENV !== "production") globalForDb.conn = conn;
export const db = drizzle(conn, { schema });

12
src/server/db/schema.ts Normal file
View File

@@ -0,0 +1,12 @@
// Example model schema from the Drizzle docs
// https://orm.drizzle.team/docs/sql-schema-declaration
import { pgTable } from "drizzle-orm/pg-core";
import {
serial,
varchar,
timestamp,
integer,
boolean
} from "drizzle-orm/pg-core";
import { sql } from "drizzle-orm";

48
src/styles/globals.css Normal file
View File

@@ -0,0 +1,48 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 10%;
--card: 0 0% 100%;
--card-foreground: 0 0% 10%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 10%;
--primary: 0 0% 10%;
--primary-foreground: 0 0% 100%;
--secondary: 0 0% 96%;
--secondary-foreground: 0 0% 10%;
--muted: 0 0% 96%;
--muted-foreground: 0 0% 45%;
--accent: 0 0% 96%;
--accent-foreground: 0 0% 10%;
--destructive: 0 84% 60%;
--destructive-foreground: 0 0% 100%;
--border: 0 0% 90%;
--input: 0 0% 90%;
--ring: 0 0% 10%;
--radius: 0.5rem;
}
}
/* Optional: Add smooth scrolling */
html {
scroll-behavior: smooth;
}
/* Optional: Improve text rendering */
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}