This repository contains the code for my personal portfolio site.
The site is built around an airport/departure-board metaphor because I wanted the portfolio to feel a little more alive than a standard grid of cards. Projects show up as destinations, photography lives in its own travel-log style pages, and the contact flow is framed more like sending a transmission than submitting a generic form.
Under the visuals, the goal is still pretty practical: keep the experience interactive and polished without turning the codebase into a mess to maintain.
Astrohandles the overall page shell and routing.Reactis used for the interactive pieces that benefit from client-side state, like the flight board, galleries, navigation, and contact form.Tailwind CSS v4is used for styling and keeping the visual system consistent.Framer Motiondrives the motion language across the site.Lenisgives the whole site smoother scrolling.Three.jswith@react-three/fiberand@react-three/dreipowers the hero starfield/background effects.Resendis used through an Astro API route for contact form delivery.
I tried to separate content, layout, and interaction cleanly:
- page composition lives in Astro
- interactive UI lives in React components
- reusable content is stored in JSON instead of hardcoded inside section components
- server-only concerns, like rate limiting and email sending, stay outside the frontend
That keeps the homepage fairly easy to reason about even though the presentation is more custom than a typical portfolio.
src/
├── components/
│ ├── astro/ # Small Astro-only building blocks
│ ├── layout/ # Cross-page layout pieces like Navigation and SmoothScroll
│ ├── sections/ # Homepage sections such as About, Journey, Contact, Arrivals
│ └── ui/ # Reusable interactive UI like FlightBoard, PhotoGallery, HeroStars
├── data/
│ ├── photography.json # Food and outdoor gallery content
│ └── projects.json # Project entries shown in the destinations section
├── layouts/
│ └── MainLayout.astro # Global shell, metadata, global CSS, Lenis mount point
├── lib/
│ └── contactRateLimit.ts # Server-side limiter for contact submissions
├── pages/
│ ├── api/contact.ts # Resend-backed contact endpoint
│ ├── food.astro # Food photography page
│ ├── index.astro # Main landing page
│ └── outdoor.astro # Outdoor photography page
└── styles/
├── flightboard.css # Split-flap board styling
└── global.css # Global theme, utilities, and site-wide styles
The content model is intentionally simple:
src/data/projects.jsondrives the projects shown in theDestinationssectionsrc/data/photography.jsondrives the food and outdoor gallery pagespublic/photography/...stores the actual image assets in WebP format
That means most edits to projects or galleries can be done without touching component logic. For example, adding a new project is usually just a new object in projects.json, and adding a new photo set is usually a new entry in photography.json.
Install dependencies:
npm installStart the dev server:
npm run devBuild for production:
npm run buildPreview the production build locally:
npm run previewIf you want the contact form to actually send mail, create a .env file using .env.example as a guide:
RESEND_API_KEY=re_your_resend_api_key
RESEND_FROM_EMAIL=contact@yourdomain.comNotes:
RESEND_FROM_EMAILis required and should be a verified sender from your Resend account.- Use a plain email address when setting
RESEND_FROM_EMAILin Vercel to avoid wrapping quotes being sent to Resend. - If you prefer
Name <email@example.com>format, enter it without surrounding quotes in the Vercel dashboard. - Do not rely on
onboarding@resend.devfor deployment; use your own verified domain/sender. - portfolio messages are sent to
vvictort20@gmail.com - the contact API also applies rate limiting at 2 submissions per 24 hours per user/IP
This project uses Astro in server output mode with the Vercel serverless adapter because the contact form depends on an API route.
The repository also includes a vercel.json so the framework and build commands are explicit in version control.
This is not a purely static deployment. To keep contact delivery working, it should be deployed to Vercel or another environment using the matching Astro adapter.
- The homepage is composed as sections in
src/pages/index.astro, while each section stays isolated insrc/components/sections. - The airport board in
src/components/ui/FlightBoard.tsxis treated like a self-contained display system rather than mixing its logic into the page. - The photo galleries in
src/components/ui/PhotoGallery.tsxgroup same-day images together so a single card can hold multiple frames. - The contact flow in
src/pages/api/contact.tsvalidates input, rate-limits submissions, and sends through Resend without relying on default browser form behavior.
- stronger persistence for rate limiting beyond a single server instance
- more structured content metadata for photography captions and locations
- further performance tuning around the heavier visual effects