A modern, performant portfolio website built with Next.js 15, TypeScript, and Supabase.
| Layer | Technology |
|---|---|
| Framework | Next.js 15.3.0 (App Router) |
| Language | TypeScript 5.x (strict mode) |
| Styling | Tailwind CSS v4 |
| Database | Supabase (PostgreSQL) |
| Auth | NextAuth.js (GitHub OAuth) |
| UI Components | Radix UI + shadcn/ui |
| Forms | react-hook-form + Zod validation |
| File Uploads | Uploadthing |
| Resend | |
| Icons | lucide-react |
| Deployment | Vercel |
{
"@radix-ui/*": "Latest",
"@shadcn/ui": "Latest",
"@supabase/supabase-js": "Latest",
"@supabase/ssr": "Latest",
"react-hook-form": "Latest",
"zod": "Latest",
"lucide-react": "Latest",
"next-auth": "Latest",
"uploadthing": "Latest",
"resend": "Latest"
}app/
├── (routes)/ # Main routes
│ ├── about/ # About page
│ ├── contact/ # Contact form
│ └── projects/ # Project showcase
├── api/ # API routes & webhooks
├── components/
│ ├── ui/ # UI components (Button, Card, etc)
│ ├── tech-icons/ # Technology badges
│ ├── navigation/ # Header/footer components
│ └── forms/ # Form components
├── layout.tsx # Root layout with nav/footer
└── lib/
├── supabase/ # Client & server Supabase instances
├── auth/ # NextAuth configuration
└── utils.ts # Helper functions
types/
├── database.types.ts # Auto-generated from Supabase
└── index.ts # Type definitions
supabase/
├── migrations/ # SQL migration files
└── functions/ # Postgres functions
app/(routes)/about - Personal bio and skills showcase
app/(routes)/projects - Dynamic project portfolio from Supabase
app/(routes)/contact - Contact form with spam protection
CREATE TABLE projects (
id UUID PRIMARY KEY,
title TEXT NOT NULL,
description TEXT NOT NULL,
short_description TEXT NOT NULL,
logo_path TEXT NOT NULL,
logo_url TEXT,
preview_image TEXT,
technologies technology[] DEFAULT '{}', -- ENUM array
git_url TEXT NOT NULL,
live_url TEXT,
framework TEXT,
featured BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);CREATE TABLE project_journey_steps (
id UUID PRIMARY KEY,
project_id UUID REFERENCES projects(id) ON DELETE CASCADE,
title TEXT NOT NULL,
description TEXT NOT NULL,
img_url TEXT,
step_order INTEGER NOT NULL,
UNIQUE(project_id, step_order)
);Includes: AWS, Axios, Docker, FastAPI, HuggingFace, Next.js, NextJS, Python, PyTorch, React, YOLOv8, and more.
- Node.js 18+ (or Bun)
- Git
- Supabase account
-
Clone the repository
git clone https://github.com/EtanHey/etanheyman.com.git cd etanheyman.com -
Install dependencies
npm install # or with bun bun install -
Set up environment variables Create
.env.local:# Supabase NEXT_PUBLIC_SUPABASE_URL=https://mkijzwkuubtfjqcemorx.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=<your-anon-key> # NextAuth NEXTAUTH_URL=http://localhost:3000 NEXTAUTH_SECRET=<generate-with: openssl rand -base64 32> GITHUB_CLIENT_ID=<your-github-oauth-id> GITHUB_CLIENT_SECRET=<your-github-oauth-secret> ALLOWED_EMAILS=<comma-separated-emails> # Services UPLOADTHING_TOKEN=<your-uploadthing-token> RESEND_API_KEY=<your-resend-api-key>
-
Generate database types
npx supabase gen types typescript --project-id mkijzwkuubtfjqcemorx > types/database.types.ts -
Start the development server
npm run dev
npm run dev # Start dev server on :3000
npm run build # Build for production
npm run start # Run production build
npm run lint # Check code quality
npm run type-check # TypeScript validation-
Create migration file in
supabase/migrations/:# Format: YYYYMMDD_descriptive_name.sql touch supabase/migrations/20250202_add_featured_column.sql -
Write migration SQL
-- supabase/migrations/20250202_add_featured_column.sql ALTER TABLE projects ADD COLUMN featured BOOLEAN DEFAULT false;
-
Apply locally and verify before committing
-
Commit migration file
git add supabase/migrations/ git commit -m "chore: add featured column to projects"
- ✅ Always create migration files before changes
- ✅ Use descriptive snake_case names
- ✅ Version control all migrations
- ✅ Never use raw SQL scripts in production
- ❌ Never modify database directly without migrations
-
Create GitHub OAuth App
- Go to GitHub Settings → Developer settings → OAuth Apps
- Create new OAuth App
- Set Authorization callback URL to:
http://localhost:3000/api/auth/callback/github
-
Add credentials to
.env.localGITHUB_CLIENT_ID=your_client_id GITHUB_CLIENT_SECRET=your_client_secret
-
Configure allowed emails
ALLOWED_EMAILS=your-email@example.com,other@example.com
// Client component
'use client';
import { useSession, signIn, signOut } from 'next-auth/react';
export default function AuthButton() {
const { data: session } = useSession();
if (session) {
return <button onClick={() => signOut()}>Sign Out</button>;
}
return <button onClick={() => signIn('github')}>Sign In</button>;
}Font Stack:
- Headers: Nutmeg (custom font)
- Body: Roboto (system font)
Responsive Sizes:
| Level | Desktop | Mobile |
|---|---|---|
| H1 | 64px | 34px |
| H2 | 48px | 26px |
| H3 | 40px | 22px |
| H4 | 32px | 20px |
| H5 | 24px | 16px |
| H6 | 20px | 15px |
| Body | 18px | 14px |
/* Tailwind CSS Classes */
bg-background /* #00003F - Main bg */
bg-primary /* #0F82EB - Primary blue */
bg-blue-700 /* #0053A4 - Dark blue (icons) */
bg-blue-50 /* #E7F5FE - Lightest blue */
bg-white /* #FFFFFF - White */
bg-red /* #E70E0E - Red/errors */<h1 className="text-[34px] md:text-[64px] font-bold font-[Nutmeg]">
<p className="text-sm md:text-lg leading-relaxed">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">-
Push to GitHub (automatic deployment)
git push origin main
-
Vercel auto-deploys on push
- Main branch → Production
- Other branches → Preview deployments
-
Environment variables Set on Vercel dashboard (Project Settings → Environment Variables):
NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEYNEXTAUTH_URL(set to production domain)NEXTAUTH_SECRETGITHUB_CLIENT_IDGITHUB_CLIENT_SECRETALLOWED_EMAILSUPLOADTHING_TOKENRESEND_API_KEY
- All env variables set on Vercel
- GitHub OAuth callback URL updated for production domain
- NextAuth secret is strong and unique
- Supabase policies are configured for production
- Email service (Resend) is properly configured
- File uploads (Uploadthing) are configured
- Backups configured in Supabase
- Monitoring/error tracking set up (optional: Sentry, LogRocket)
// app/page.tsx
import { createClient } from '@/lib/supabase/server';
export default async function Page() {
const supabase = await createClient();
const { data: projects } = await supabase
.from('projects')
.select('*')
.eq('featured', true);
return <div>{/* render */}</div>;
}'use client';
import { createClient } from '@/lib/supabase/client';
export default function Component() {
const supabase = createClient();
const [data, setData] = useState(null);
useEffect(() => {
const fetchProjects = async () => {
const { data } = await supabase.from('projects').select('*');
setData(data);
};
fetchProjects();
}, []);
return <div>{/* render */}</div>;
}// app/api/projects/route.ts
import { createClient } from '@/lib/supabase/server';
export async function GET() {
try {
const supabase = await createClient();
const { data, error } = await supabase.from('projects').select('*');
if (error) throw error;
return Response.json(data);
} catch (error) {
return Response.json(
{ error: error.message },
{ status: 500 }
);
}
}'use client';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
const schema = z.object({
email: z.string().email(),
message: z.string().min(10),
});
export default function ContactForm() {
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(schema),
});
const onSubmit = async (data) => {
// Handle submission
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
{/* form fields */}
</form>
);
}- Navigation: Header and footer are in root layout—don't modify them directly
- Background: Handled by layout component
- Testing: Always test on both mobile (375px) and desktop (1440px+)
- Icons: Use
lucide-reactonly—don't create custom SVGs unless absolutely necessary - Database Types: Auto-generated from Supabase—never manually edit
database.types.ts - RTL: Hebrew/Arabic UI needs explicit RTL checks (flex order reversal, text alignment)
This is a personal portfolio, but contributions for improvements are welcome. Please ensure:
- Code passes TypeScript strict mode
- All tests pass (
npm run lint) - Components follow existing patterns
- Database changes include migrations
Personal project. See LICENSE file for details.
For questions about the portfolio, visit the GitHub repository or check the design files.