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

108
.gitignore vendored Normal file
View File

@@ -0,0 +1,108 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
jspm_packages/
# Include specific dependencies
!node_modules/
node_modules/*
!node_modules/@fortawesome
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port

38
Dockerfile Normal file
View File

@@ -0,0 +1,38 @@
# Use the Node.js 18 Alpine Linux image as the base image
FROM node:22-alpine
# Install GraphicsMagick
RUN apk add --no-cache graphicsmagick ghostscript
# Add LaTeX support
RUN apk add --no-cache \
texlive \
texlive-latex \
texlive-xetex \
texmf-dist-latexextra
# Set the working directory inside the container to /app
WORKDIR /app
# Copy package.json and package-lock.json files into the working directory
COPY package*.json ./
# Install the dependencies specified in package.json
RUN npm install -g pnpm
RUN pnpm install
# Copy all the files from the local directory to the working directory in the container
COPY . .
# # Clear previous build artifacts
# RUN rm -rf .next
# # Build the application
# RUN pnpm build
# # Ensure correct permissions
# RUN chown -R node:node .
# USER node
# Run the application in development mode
CMD ["pnpm", "run", "dev"]

80
README.md Normal file
View File

@@ -0,0 +1,80 @@
# HRIStudio
HRIStudio is a web-based platform designed to streamline the design, execution, and analysis of Wizard-of-Oz (WoZ) experiments in Human-Robot Interaction (HRI) studies. It 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.
## Features
- User-friendly graphical interface for designing WoZ experiments
- Visual programming environment with drag-and-drop functionality
- Real-time control and observation capabilities during live experiment sessions
- Comprehensive data logging and playback tools
- Integration with Robot Operating System (ROS) for various robotic platforms
- Collaborative workflow support with multiple user accounts and data sharing
## System Requirements
- Node.js (version X.X.X or higher)
- npm (version X.X.X or higher)
- ROS (Robot Operating System)
## Installation
1. Clone the repository:
```
git clone https://github.com/your-username/hristudio.git
```
2. Navigate to the project directory:
```
cd hristudio
```
3. Install dependencies:
```
npm install
```
4. Set up environment variables:
Create a `.env.local` file in the root directory and add the necessary environment variables (e.g., database connection string, API keys).
5. Run the development server:
```
npm run dev
```
6. Open [http://localhost:3000](http://localhost:3000) in your browser to see the application.
## Project Structure
- `pages/`: Contains the Next.js pages
- `components/`: Reusable React components
- `public/`: Static assets
- `styles/`: CSS styles
- `lib/`: Utility functions and modules
- `api/`: API routes
- `ros/`: ROS interface and related components
## Usage
1. Create a new study in the Dashboard view
2. Design your experiment using the visual programming interface in the Design view
3. Execute the experiment using the Execute view
4. Analyze results and playback recorded data in the Playback view
For detailed usage instructions, please refer to the [User Guide](link-to-user-guide).
## Contributing
We welcome contributions to HRIStudio! Please read our [Contributing Guidelines](link-to-contributing-guidelines) for more information on how to get started.
## License
This project is licensed under the [MIT License](link-to-license-file).
## Contact
For questions or support, please contact [your-email@example.com](mailto:your-email@example.com).
## Acknowledgments
This project is being developed by Sean O'Connor and L. Felipe Perrone at Bucknell University. We would like to thank the robotics and HRI research community for their valuable insights and contributions.

20
components.json Normal file
View File

@@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/styles/globals.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "~/components",
"utils": "~/lib/utils",
"ui": "~/components/ui",
"lib": "~/lib",
"hooks": "~/hooks"
}
}

238
cv.tex Normal file
View File

@@ -0,0 +1,238 @@
\documentclass{article}
\setlength{\parindent}{0pt}
\setlength{\parskip}{0em}
\usepackage{enumitem}
\usepackage{hyperref}
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\usepackage[left=0.5in,top=0.5in,right=0.5in,bottom=0.5in]{geometry}
% Define spacing variables
\newlength{\sectspaceabove}
\newlength{\sectspacebelow}
\setlength{\sectspaceabove}{-0em} % Space before section titles
\setlength{\sectspacebelow}{-0.5em} % Space after section titles
\usepackage{etoolbox}
\patchcmd{\thebibliography}{\section*{\refname}}{}{}{}
% Hide page numbers
\pagestyle{empty}
% Custom macro for small caps and bold
\newcommand{\textscbf}[1]{\textbf{\textsc{#1}}}
% Custom macro for section headers with controlled spacing
\newcommand{\resumesection}[1]{%
\vspace{\sectspaceabove}%
\begin{center}
\textscbf{#1}
\end{center}%
\vspace{\sectspacebelow}%
}
\begin{document}
\begin{center}
{\large \textscbf{Sean O'Connor}}
\end{center}
\noindent
\begin{minipage}[t]{0.33\textwidth}
\raggedright
Bucknell University \\
701 Moore Ave C2703 \\
Lewisburg, PA 17837
\end{minipage}%
\begin{minipage}[t]{0.33\textwidth}
\centering
sean@soconnor.dev \\
sso005@bucknell.edu \\
+1 (631) 601-6555 \\
\href{https://soconnor.dev}{www.soconnor.dev}
\end{minipage}%
\begin{minipage}[t]{0.33\textwidth}
\raggedleft
Home Address \\
14 Washington Avenue \\
Miller Place, NY 11764
\end{minipage}
\vspace{1em}
% Professional Summary Section
\noindent
\begin{minipage}{\textwidth}
Computer Science and Engineering student with extensive experience in software development, robotics research, and technical leadership.
Demonstrated track record of building scalable solutions and leading cross-functional teams.
Published researcher in human-robot interaction with experience in both academic and commercial software development.
\end{minipage}
%\vspace{1em}
\resumesection{Education}
\textscbf{Bucknell University} \hfill \textscbf{Lewisburg, PA}
\textbf{Bachelor of Science in Computer Science and Engineering} \hfill \textbf{Expected Graduation: May 2026}
\begin{itemize}[noitemsep,topsep=2pt]
\item Cumulative Engineering GPA: 3.90. Dean's List: Fall 2022, Fall 2023, Spring 2024
\end{itemize}
\resumesection{Experience}
\textscbf{Riverhead Raceway} \hfill \textscbf{Riverhead, NY}
\textbf{Software Developer} \hfill \textbf{Oct 2020 Present}
\begin{itemize}[noitemsep,topsep=2pt]
\item Engineered a digital registration platform that modernized paper-based processes, integrating payment processing and real-time number availability checking, eliminating manual processing delays
\item Built and deployed a high-performance race statistics platform serving 1500+ concurrent users, providing real-time access to driver positions, rankings, and lineups, replacing physical bulletin boards
\item Developed an intuitive content management system tailored for non-technical staff, enabling content management through familiar interfaces while maintaining website consistency
\item Orchestrated migration to containerized architecture using Docker and implemented automated backup systems to improve reliability
\end{itemize}
\textbf{IT Administrator} \hfill \textbf{Oct 2020 - Apr 2024}
\begin{itemize}[noitemsep,topsep=2pt]
\item Engineered migration from consumer desktop computers to enterprise thin clients with virtualization servers, improving reliability and remote access capabilities enabling continued support while away at university
\item Implemented automated backup solutions using the Backblaze platform with version control and disaster recovery procedures
\item Deployed Windows Server with Active Directory for centralized user management and file storage
\item Established standardized workstation images and software deployment protocols across facilities
\end{itemize}
\textbf{Media Producer} \hfill \textbf{Oct 2020 - Apr 2024}
\begin{itemize}[noitemsep,topsep=2pt]
\item Designed and deployed facility's first multi-camera live streaming system with ATEM production switchers and custom graphics pipeline
\item Developed real-time graphics integration system connecting race timing data to broadcast overlays
\item Operated replay and instant highlight system for live broadcast to FloRacing and NBC Sports networks
\item Managed live production during race events, coordinating camera operators, replay, and graphics control\\
\end{itemize}
\textscbf{Bucknell University} \hfill \textscbf{Lewisburg, PA}
\textbf{Computer Science Researcher - Human-Robot Interaction} \hfill \textbf{Jan 2023 Present}
\begin{itemize}[noitemsep,topsep=2pt]
\item Engineered a modular web-based experimental platform for human-robot interaction studies using the Wizard of Oz experimental paradigm and ROS2 and C++/Python
\item Published and presented a first-author paper and poster at the 33rd IEEE International Conference on Robot and Human Interactive Communication
\end{itemize}
\textbf{Computer Science Research Assistant - Chemical Engineering Department} \hfill \textbf{Aug 2023 Present}
\begin{itemize}[noitemsep,topsep=2pt]
\item Designed and implemented an automated data collection system using a microcontroller and C++ to collect real-time temperature, pressure, and humidity data in harsh environments
\item Currently integrating robotic arm into existing coffee research project to automate repeated brewing-related tasks and data collection, freeing up researchers from unskilled repetitive work
\end{itemize}
\textbf{Computer Science Teaching Assistant} \hfill \textbf{Jan 2024 - Present}
\begin{itemize}[noitemsep,topsep=2pt]
\item Led lecture and lab sections focusing on agile development practices and following scrum guidelines for group work in the field of computer science.
\item Assisted students with classwork, homework, and lab assignments, focusing on teaching students how to find the answers to their questions using existing documentation
\end{itemize}
\textbf{Engineering Study Spot Tutor - Computer Science} \hfill \textbf{Aug 2024 - Present}
\begin{itemize}[noitemsep,topsep=2pt]
\item Held drop-in help sessions for computer science students throughout all stages of the curriculum, assisting with introductory courses, software engineering, and systems programming assignments
\end{itemize}
\textbf{Engineering Teaching Assistant} \hfill \textbf{Aug 2023 - Dec 2023}
\begin{itemize}[noitemsep,topsep=2pt]
\item Led recurring workshops on Arduino-based microcontroller programming, assembly, and wiring for multidisciplinary student engineering projects
\item Assisted students during class and lab sections on their design session projects, with emphasis on engineering ethics education
\end{itemize}
\textbf{Physics Teaching Assistant} \hfill \textbf{Aug 2023 - May 2024}
\begin{itemize}[noitemsep,topsep=2pt]
\item Assisted students during laboratory sections with introductory and exploratory physics lab experiments, working with industry-standard data collection and analysis tools\\
\end{itemize}
\textscbf{Miller Place School District} \hfill \textscbf{Miller Place, NY}
\textbf{Information Technology Intern} \hfill \textbf{Sep 2020 - May 2022}
\begin{itemize}[noitemsep,topsep=2pt]
\item Worked under senior technical staff to assist faculty, staff and students with district-owned printers and computers
\item Assisted staff in one-laptop per person deployment and support in response to the COVID-19 pandemic, teaching students how to fully utilize newly-available remote learning tools and programs
\end{itemize}
\resumesection{Activities}
\textscbf{AIChE Chem-E-Car Competition Team} \hfill \textscbf{Lewisburg, PA}
\textbf{President, Electrical and Mechanical Team Lead} \hfill \textbf{Jan 2023 Present}
\begin{itemize}[noitemsep,topsep=2pt]
\item Pioneered team's first custom hardware solution: designed and fabricated a microcontroller-based control system with isolated power circuits for hydrogen fuel cell regulation
\item Implemented finite state machine architecture integrating spectrometer readings, relay control, and LED feedback for real-time reaction monitoring in isolated chamber conditions
\end{itemize}
\textscbf{Bucknell Coffee Society} \hfill \textscbf{Lewisburg, PA}
\textbf{Treasurer} \hfill \textbf{Oct 2023 Present}
\begin{itemize}[noitemsep,topsep=2pt]
\item Co-established and launched a new campus organization, managing financial operations and coordinating event logistics.
\item Presented on ongoing research for publication by Bucknell's student story, engineering report, and fall magazine
\end{itemize}
\textscbf{RoboLab@Bucknell} \hfill \textscbf{Lewisburg, PA}
\textbf{Founding Member} \hfill \textbf{Sep 2023 - Present}
\begin{itemize}[noitemsep,topsep=2pt]
\item 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
\end{itemize}
\resumesection{Conferences and Competitions}
\textscbf{IEEE International Conference on Robot and Human Interactive Communication} \hfill \textscbf{Aug 2024}
\begin{itemize}[noitemsep,topsep=2pt]
\item Presented a first-author paper in a poster session regarding my project HRIStudio, a novel tool enabling human-robot interaction experiments to be conducted by those unfamiliar with robotic platforms and programming
\end{itemize}
\textbf{AIChE Annual Student Conference} \hfill \textscbf{Oct 2024}
\begin{itemize}[noitemsep,topsep=2pt]
\item Competed in the 2024 National AIChE Chem-E-Car Performance Competition with Bucknell's car, H\textsubscript{2}Go
\item Presented the design of our car in a poster session, heavily focusing on the safety-related aspects of our design
\end{itemize}
\textbf{AIChE Mid-Atlantic Regional Conference} \hfill \textscbf{Apr 2024}
\begin{itemize}[noitemsep,topsep=2pt]
\item Placed second overall in the 2024 Mid-Atlantic AIChE Chem-E-Car Performance Competition with our car, H\textsubscript{2}Go
\item Presented the design of our car in a poster session, heavily focusing on the safety-related aspects of our design
\end{itemize}
\textbf{Specialty Coffee Exposition} \hfill \textscbf{Mar 2024}
\begin{itemize}[noitemsep,topsep=2pt]
\item Attended as a representative of the Bucknell Coffee Society, meeting with vendors and equipment manufacturers to request sponsorship, donations, and educational materials for our club
\end{itemize}
\textbf{AIChE Annual Student Conference} \hfill \textscbf{Oct 2023}
\begin{itemize}[noitemsep,topsep=2pt]
\item Attended as a representative of Bucknell's Chem-E-Car team, discussing designs and reactions with other teams to kickstart development of the next year's car
\end{itemize}
\textbf{AIChE Mid-Atlantic Regional Conference} \hfill \textscbf{Apr 2023}
\begin{itemize}[noitemsep,topsep=2pt]
\item Competed in the 2023 Mid-Atlantic AIChE Chem-E-Car Performance Competition with our car, H\textsubscript{2}Go
\item Presented the design of our car in a poster session, heavily focusing on the safety-related aspects of our design
\end{itemize}
\resumesection{Publications}
\nocite{*}
\bibliography{subfiles/refs.bib}
\bibliographystyle{plain}
\resumesection{Relevant Coursework}
\textbf{Systems \& Architecture:} Computer Systems, Operating Systems Design, Computer Networks \& Security
\textbf{Software Development:} Software Engineering, Data Structures \& Algorithms, Research Methods, Ethics in Computing
\textbf{Mathematics:} Calculus II, Linear Algebra, Discrete Mathematics, Statistics, Applied Statistics with R, Data Mining
\resumesection{Skills \& Interests}
\textbf{Languages \& Frameworks:} Java, C/C++, Python, JavaScript/TypeScript, React, Next.js, PHP, SQL
\textbf{Backend \& DevOps:} REST APIs, MySQL, PostgreSQL, Docker, Apache Web Server, NGINX, ROS2
\textbf{Cloud \& Infrastructure:} AWS, GCP, Azure, Backblaze, Linux (RHEL/Debian), CI/CD
\textbf{Development Tools:} Git, JetBrains Suite, VS Code, Cursor, Linux CLI
\end{document}

37
docker-compose.yml Normal file
View File

@@ -0,0 +1,37 @@
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- '3000:3000' # Node.js
# - '4983:4983' # Drizzle Studio
volumes:
- .:/app
- /app/node_modules
environment:
NODE_ENV: development
command: ["sh", "-c", "pnpm db:push && pnpm run dev"]
depends_on:
- db
db:
image: postgres
restart: always
# ports:
# - 5432:5432 # DEBUG
volumes:
- postgres:/var/lib/postgresql/data
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
adminer:
image: adminer
restart: always
ports:
- 8080:8080
volumes:
postgres:

12
drizzle.config.ts Normal file
View File

@@ -0,0 +1,12 @@
import { type Config } from "drizzle-kit";
import { env } from "~/env";
export default {
schema: "./src/server/db/schema.ts",
dialect: "postgresql",
dbCredentials: {
url: env.DATABASE_URL,
},
// tablesFilter: ["hristudio_*"],
} satisfies Config;

5
next-env.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.

41
next.config.js Normal file
View File

@@ -0,0 +1,41 @@
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful
* for Docker builds.
*/
await import("./src/env.js");
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
experimental: {
serverActions: {
bodySizeLimit: '2mb',
},
},
webpack: (config) => {
config.externals.push({
"utf-8-validate": "commonjs utf-8-validate",
bufferutil: "commonjs bufferutil",
});
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*',
},
];
},
};
export default nextConfig;

70
package.json Normal file
View File

@@ -0,0 +1,70 @@
{
"name": "personal-website",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"build": "next build",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"dev": "next dev",
"lint": "next lint",
"start": "next start"
},
"dependencies": {
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-tooltip": "^1.1.2",
"@react-pdf/renderer": "^3.4.0",
"@t3-oss/env-nextjs": "^0.10.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cn": "^0.1.1",
"drizzle-orm": "^0.33.0",
"geist": "^1.3.1",
"lucide-react": "^0.441.0",
"next": "^14.2.13",
"next-themes": "^0.3.0",
"pdf2pic": "^3.1.3",
"postgres": "^3.4.4",
"radix-ui": "^1.0.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^5.3.0",
"spawn-sync": "^2.0.0",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/eslint": "^8.56.12",
"@types/node": "^20.16.5",
"@types/react": "^18.3.7",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^8.6.0",
"@typescript-eslint/parser": "^8.6.0",
"drizzle-kit": "^0.24.2",
"eslint": "^8.57.1",
"eslint-config-next": "^14.2.12",
"eslint-plugin-drizzle": "^0.2.3",
"postcss": "^8.4.47",
"prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.6",
"tailwindcss": "^3.4.12",
"typescript": "^5.6.2"
},
"ct3aMetadata": {
"initVersion": "7.37.0"
},
"packageManager": "pnpm@9.9.0"
}

6372
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

7
postcss.config.cjs Normal file
View File

@@ -0,0 +1,7 @@
const config = {
plugins: {
tailwindcss: {},
},
};
module.exports = config;

6
prettier.config.js Normal file
View File

@@ -0,0 +1,6 @@
/** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */
const config = {
plugins: ["prettier-plugin-tailwindcss"],
};
export default config;

BIN
public/cv.pdf Normal file

Binary file not shown.

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
public/headshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 KiB

BIN
public/hristudio_laptop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

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;
}

55
start-database.sh Executable file
View File

@@ -0,0 +1,55 @@
#!/usr/bin/env bash
# Use this script to start a docker container for a local development database
# TO RUN ON WINDOWS:
# 1. Install WSL (Windows Subsystem for Linux) - https://learn.microsoft.com/en-us/windows/wsl/install
# 2. Install Docker Desktop for Windows - https://docs.docker.com/docker-for-windows/install/
# 3. Open WSL - `wsl`
# 4. Run this script - `./start-database.sh`
# On Linux and macOS you can run this script directly - `./start-database.sh`
DB_CONTAINER_NAME="personal-website-postgres"
if ! [ -x "$(command -v docker)" ]; then
echo -e "Docker is not installed. Please install docker and try again.\nDocker install guide: https://docs.docker.com/engine/install/"
exit 1
fi
if [ "$(docker ps -q -f name=$DB_CONTAINER_NAME)" ]; then
echo "Database container '$DB_CONTAINER_NAME' already running"
exit 0
fi
if [ "$(docker ps -q -a -f name=$DB_CONTAINER_NAME)" ]; then
docker start "$DB_CONTAINER_NAME"
echo "Existing database container '$DB_CONTAINER_NAME' started"
exit 0
fi
# import env variables from .env
set -a
source .env
DB_PASSWORD=$(echo "$DATABASE_URL" | awk -F':' '{print $3}' | awk -F'@' '{print $1}')
DB_PORT=$(echo "$DATABASE_URL" | awk -F':' '{print $4}' | awk -F'\/' '{print $1}')
if [ "$DB_PASSWORD" = "password" ]; then
echo "You are using the default database password"
read -p "Should we generate a random password for you? [y/N]: " -r REPLY
if ! [[ $REPLY =~ ^[Yy]$ ]]; then
echo "Please change the default password in the .env file and try again"
exit 1
fi
# Generate a random URL-safe password
DB_PASSWORD=$(openssl rand -base64 12 | tr '+/' '-_')
sed -i -e "s#:password@#:$DB_PASSWORD@#" .env
fi
docker run -d \
--name $DB_CONTAINER_NAME \
-e POSTGRES_USER="postgres" \
-e POSTGRES_PASSWORD="$DB_PASSWORD" \
-e POSTGRES_DB=personal-website \
-p "$DB_PORT":5432 \
docker.io/postgres && echo "Database container '$DB_CONTAINER_NAME' was successfully created"

63
tailwind.config.ts Normal file
View File

@@ -0,0 +1,63 @@
import { type Config } from "tailwindcss";
import { fontFamily } from "tailwindcss/defaultTheme";
export default {
darkMode: ["class"],
content: ["./src/**/*.tsx"],
theme: {
extend: {
fontFamily: {
sans: ["var(--font-sans)"],
unineue: ["var(--font-unineue)"]
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
},
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
chart: {
'1': 'hsl(var(--chart-1))',
'2': 'hsl(var(--chart-2))',
'3': 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))'
}
}
}
},
plugins: [require("tailwindcss-animate")],
} satisfies Config;

51
tsconfig.json Normal file
View File

@@ -0,0 +1,51 @@
{
"compilerOptions": {
/* Base Options: */
"esModuleInterop": true,
"skipLibCheck": true,
"target": "es2022",
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
/* Strictness */
"strict": true,
"noUncheckedIndexedAccess": true,
"checkJs": true,
/* Bundled projects */
"lib": [
"dom",
"dom.iterable",
"ES2022"
],
"noEmit": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "preserve", // or "react" for older versions
"plugins": [
{
"name": "next"
}
],
"incremental": true,
/* Path Aliases */
"baseUrl": ".",
"paths": {
"~/*": [
"./src/*"
]
}
},
"include": [
".eslintrc.cjs",
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"**/*.cjs",
"**/*.js",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}