A personal portfolio built with React and Vite that showcases projects, blogs, skills, GitHub activity, and a small visitor counter in the footer.
- React 19
- Vite
- TypeScript
- Tailwind CSS v4
- shadcn/ui
- Framer Motion
- React Router
- next-themes
- Vercel serverless functions
- Supabase for visitor storage
- Home page with hero, skills, featured projects, featured blogs, GitHub activity, and a quote section
- Dedicated pages for
projects,blogs, andcontact - Detail routes for individual projects and blog posts
- Theme switching with system, light, and dark support
- Smooth scrolling and motion-driven section reveals
- Footer visitor tracking with a locally generated fingerprint
/home/projectsall projects/projects/:slugproject detail/blogsall blogs/blogs/:slugblog detail/contactcontact page
The footer visitor counter is handled in two parts:
- The client creates a local fingerprint in
src/lib/fingerprint.ts. - That fingerprint is sent to the serverless endpoint at
api/visitors.ts.
The user-agent is explicitly part of the fingerprint input. It is read from navigator.userAgent, combined with other browser/device signals, hashed locally, and stored in localStorage under visitor_id.
The fingerprint inputs are:
navigator.userAgentnavigator.languagescreen.width x screen.height- browser timezone from
Intl.DateTimeFormat().resolvedOptions().timeZone navigator.hardwareConcurrencynavigator.deviceMemorywhen available- a canvas-based hash generated in the browser
Important behavior:
- If
localStorage.visitor_idalready exists, the app reuses it. - The raw fingerprint inputs are not sent to the API.
- Only the hashed
visitor_idis posted to/api/visitors.
The visitor API:
- accepts
POSTrequests with{ visitor_id } - inserts the visitor into Supabase with duplicate protection
- reads the total visitor count from Supabase
- returns the visitor's ordinal position plus the total count
If Supabase env vars are missing or the API fails, the UI falls back gracefully and keeps the footer quiet.
The Vite dev server proxies /api/* to http://localhost:3000, which is where the Vercel serverless runtime runs. You need two terminals to have the visitor counter working locally.
- Bun
- Vercel CLI — install globally once:
npm i -g vercel
-
Install dependencies:
bun install
-
Create a
.envfile in the project root with your Supabase credentials:SUPABASE_URL=https://<your-project-ref>.supabase.co SUPABASE_SERVICE_ROLE_KEY=<your-service-role-key>
Terminal 1 — Vite frontend:
bun run devTerminal 2 — Vercel serverless API (visitor counter):
vercel dev --listen 3000The Vite config proxies
/api→http://localhost:3000, so both servers must be running for the visitor counter to work. If the API is not running, the footer counter silently stays hidden — everything else works fine.
bun run buildThe visitor endpoint reads these at runtime (set them in .env locally, and in the Vercel project dashboard for production):
| Variable | Description |
|---|---|
SUPABASE_URL |
Your Supabase project URL |
SUPABASE_SERVICE_ROLE_KEY |
Supabase service role key (server-side only) |
src/components/reusable UI and section componentssrc/pages/route-level pagessrc/data/portfolio content for projects, blogs, socials, and techsrc/lib/shared utilities including fingerprint generationapi/Vercel serverless endpointspublic/static assets
- Most of the main page content is data-driven, so updating
src/data/*changes the public portfolio content without touching layout code. - The
SUPABASE_SERVICE_ROLE_KEYhas full DB access — never expose it client-side. It is only used insideapi/visitors.tswhich runs server-side.