A modern, performant portfolio website built with Next.js 15, TypeScript, and Supabase.
| Layer | Technology |
|---|---|
| Framework | Next.js 15 (App Router, Turbopack) |
| Language | TypeScript 5.x (strict mode) |
| Styling | Tailwind CSS v4 |
| Database | Supabase (PostgreSQL) |
| Auth | NextAuth.js (GitHub OAuth) |
| UI Components | Radix UI + shadcn/ui |
| Animations | Framer Motion |
| Content | MDX (next-mdx-remote, rehype, remark) |
| File Uploads | Uploadthing |
| Resend | |
| Icons | lucide-react |
| Deployment | Vercel |
{
"@radix-ui/*": "Latest",
"@supabase/supabase-js": "Latest",
"@supabase/ssr": "Latest",
"framer-motion": "Latest",
"next-mdx-remote": "Latest",
"lucide-react": "Latest",
"next-auth": "Latest",
"uploadthing": "Latest",
"resend": "Latest"
}app/
├── (portfolio)/ # Portfolio routes
│ ├── about/ # About page
│ ├── contact/ # Contact form
│ ├── projects/ # Project showcase (list, [slug], add)
│ ├── admin/ # Admin panel
│ ├── api/ # API routes (auth, projects, uploadthing)
│ └── unauthorized/ # Auth error page
├── (golems)/ # Golems documentation site
│ └── golems/ # Docs, skills browser
├── components/
│ ├── ui/ # UI components (Button, Card, etc)
│ ├── tech-icons/ # Technology badges
│ ├── navigation/ # Header/footer components
│ └── contact/ # Contact form components
├── layout.tsx # Root layout with nav/footer
└── lib/
└── supabase/ # Client & server Supabase instances
types/
├── database.types.ts # Auto-generated from Supabase
├── auth.ts # Auth type definitions
└── next-auth.d.ts # NextAuth type augmentations
supabase/
└── migrations/ # SQL migration files
app/(portfolio)/about — Personal bio and skills showcase
app/(portfolio)/projects — Dynamic project portfolio from Supabase (with per-project architecture, docs, features sub-pages)
app/(portfolio)/contact — Contact form with spam protection
app/(golems)/golems — Golems ecosystem documentation and skills browser
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=[email protected],[email protected]
// 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 }
);
}
}// Simplified from app/(portfolio)/contact/actions.ts
'use server';
import { Resend } from 'resend';
export async function submitContactForm(data: ContactFormData) {
const resend = new Resend(process.env.RESEND_API_KEY);
await resend.emails.send({
from: 'Portfolio <[email protected]>',
to: process.env.CONTACT_EMAIL!,
subject: `Contact from ${data.fullName}`,
react: EmailTemplate(data),
});
return { success: true };
}- 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.