Tutorial-like documentation of Lazy-Inviter App
Lazy Inviter — Project Tutorial
A beginner-friendly walkthrough of the entire codebase: how it works, why each piece exists, and how to build it yourself.
Table of Contents
- What is this project?
- Tech stack at a glance
- Project structure
- Environment & configuration
- Database schema (Supabase)
- Authentication — proxy.ts & login
- Routing & layouts (Next.js App Router)
- Data-driven theming
- Shared library code
- Reusable UI components
- The Birthday Wizard (creation flow)
- AI idea generation (Claude API)
- The Organizer Dashboard
- Project Detail (editing & management)
- Public invitation & RSVP
- To-do lists with AI suggestions
- PDF export & printing
- QR code & sharing
- Guest reconciliation on edit
- Dark theme support
- How to replicate this project from scratch
1. What is this project?
Lazy Inviter is a web app for creating personalised children's birthday party invitations. You pick a theme (Unicorn, Spider-Man, Dinosaur, …), fill in the party details, and let Claude AI generate a creative invitation text, decoration ideas, activity suggestions, and a dress code — all in German (Swiss context). The app then produces a shareable link with QR code so guests can RSVP by phone number.
Beyond creation, an organizer dashboard lets you manage multiple invitations, track RSVP status, edit all fields inline, manage to-do lists (with AI-generated task suggestions), archive old invitations, and export print-ready PDFs.
This is a real-world side project, but it demonstrates many patterns that apply to any full-stack Next.js + Supabase application.
2. Tech stack at a glance
| Technology | What it does here | Learn more |
|---|---|---|
| Next.js 16 (App Router) | Framework — file-based routing, server components, API routes | 📖 Next.js docs |
| React 19 | UI library — components, hooks, state management | 📖 React docs |
| TypeScript 5 | Type safety across the entire codebase | 📖 TypeScript handbook |
| Supabase (Postgres) | Database, Row Level Security, anon + service clients | 📖 Supabase docs |
| Claude API (Anthropic) | AI-generated invitation text, party ideas, and to-do tasks | 📖 Anthropic API docs |
| html2canvas-pro + jsPDF | Client-side DOM → Canvas → PDF export | 📖 jsPDF docs |
| qrcode.react | QR code SVG generation for shareable links | 📖 qrcode.react |
| nanoid | Generates short, URL-safe unique tokens for invitations | 📖 nanoid |
| Tailwind CSS 4 | Minimal usage — mainly imports; styling is inline | 📖 Tailwind docs |
3. Project structure
lazy-inviter/
├── app/ # Next.js App Router root
│ ├── layout.tsx # Root layout (fonts, Navbar, Footer)
│ ├── page.tsx # "/" — Dashboard (server component)
│ ├── globals.css # Animations, print styles
│ ├── fonts.ts # Google Fonts setup
│ ├── create/page.tsx # "/create" — Birthday Wizard
│ ├── login/page.tsx # "/login" — Password gate
│ ├── project/[token]/page.tsx # "/project/:token" — Detail view
│ ├── invite/[token]/ # "/invite/:token" — Public RSVP
│ │ ├── page.tsx # Server component (data fetch)
│ │ └── InviteClient.tsx # Client component (RSVP form)
│ ├── api/ # API Route Handlers
│ │ ├── generate/route.ts # POST — Claude AI idea generation
│ │ ├── invitations/ # GET (list) + POST (create)
│ │ │ └── [token]/route.ts # GET + PATCH + DELETE per invitation
│ │ ├── login/route.ts # POST — password verification
│ │ ├── rsvp/route.ts # POST — guest RSVP submission
│ │ └── todos/ # GET + POST (list & create)
│ │ ├── [id]/route.ts # PATCH + DELETE per todo
│ │ └── generate/route.ts # POST — Claude AI todo generation
│ ├── components/ # All React components
│ │ ├── BirthdayWizard.tsx # 3-step creation wizard
│ │ ├── Dashboard.tsx # Invitation list + search/filter
│ │ ├── ProjectDetail.tsx # Full project editing view
│ │ ├── InvitationCard.tsx # Digital invitation preview
│ │ ├── PrintInvitation.tsx # Print-optimised invitation layout
│ │ ├── EditableField.tsx # Click-to-edit text + list
│ │ ├── GuestListManager.tsx # Add/remove guests
│ │ ├── RsvpStatusTable.tsx # RSVP progress + guest table
│ │ ├── TodoList.tsx # Checklist with AI generation
│ │ ├── ShareSection.tsx # QR code + copy link
│ │ ├── FloatingSymbols.tsx # Decorative background animation
│ │ ├── StepIndicator.tsx # Wizard progress indicator
│ │ ├── ProgressBar.tsx # Segmented progress bar
│ │ ├── ThemeIcon.tsx # Image (SVG) or emoji fallback
│ │ ├── Navbar.tsx # Sticky header navigation
│ │ ├── Footer.tsx # Copyright footer
│ │ └── ui.tsx # Card, Field, Button, etc.
│ └── lib/ # Shared utilities
│ ├── types.ts # All TypeScript interfaces
│ ├── themes.ts # 8 theme definitions
│ ├── supabase.ts # Anon + service client factories
│ └── format.ts # Date formatting (de-CH)
├── public/themes/ # Theme SVG illustrations
├── supabase/schema.sql # Full database schema
├── proxy.ts # Auth middleware (Next.js 16)
├── .env.example # Required environment variables
└── package.json
The app/ folder is the URL map. Every page.tsx inside
it corresponds to a route. Think of it like a filing cabinet
where each drawer's label is the URL path.
4. Environment & configuration
The app needs five environment variables, defined in
.env.local (never committed):
# Anthropic API key — used to call Claude for AI generation
ANTHROPIC_API_KEY=sk-ant-...
# Simple password to protect the organizer dashboard
APP_PASSWORD=your-secret-password
# Base URL for constructing shareable links
NEXT_PUBLIC_BASE_URL=http://localhost:3000
# Supabase connection — project URL + keys
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=eyJ...
SUPABASE_SERVICE_ROLE_KEY=eyJ...
The NEXT_PUBLIC_ prefix makes a variable available in
client-side code. All others are server-only.
📖 Next.js environment variables
5. Database schema (Supabase)
Three tables, all using UUID primary keys and cascading deletes:
invitations
├── id UUID (PK)
├── token TEXT UNIQUE -- nanoid, used in URLs
├── form_data JSONB -- child name, date, location…
├── ideas_data JSONB -- tagline, text, decorations…
├── theme_id TEXT -- e.g. "spiderman"
├── archived BOOLEAN -- soft-delete for dashboard
├── created_at TIMESTAMPTZ
└── updated_at TIMESTAMPTZ
guests
├── id UUID (PK)
├── invitation_id UUID → invitations(id) ON DELETE CASCADE
├── name TEXT
├── phone TEXT
├── status TEXT -- 'pending'|'accepted'|'declined'|'maybe'
├── responded_at TIMESTAMPTZ
└── created_at TIMESTAMPTZ
todos
├── id UUID (PK)
├── invitation_id UUID → invitations(id) ON DELETE CASCADE
├── title TEXT
├── due_date DATE
├── completed BOOLEAN
├── ai_generated BOOLEAN
├── sort_order INTEGER
└── created_at TIMESTAMPTZ
Row Level Security is enabled on all three tables. The anon key can read everything (needed for public invite pages and RSVP matching), and update guest status. All writes to invitations and todos use the service role key, which bypasses RLS entirely.
This is the "two-client" pattern:
// lib/supabase.ts
export function createClient() { /* anon key */ }
export function createServiceClient() { /* service key */ }
The anon client is used for public reads. The service client is used in server-side API routes for any write operation.
6. Authentication — proxy.ts & login
Next.js 16 uses proxy.ts (not the older middleware.ts).
It intercepts every request and checks for an auth cookie:
// proxy.ts
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl;
// These routes are always public:
if (
pathname.startsWith("/invite/") ||
pathname.startsWith("/api/rsvp") ||
pathname.startsWith("/themes/") ||
pathname === "/login" ||
// ...
) {
return NextResponse.next();
}
// Check for auth cookie
const authCookie =
request.cookies.get("lazy-inviter-auth");
if (authCookie?.value === "authenticated") {
return NextResponse.next();
}
// Bounce to login page
return NextResponse.redirect(
new URL("/login", request.url)
);
}
The login API route (/api/login) compares the submitted
password against APP_PASSWORD and sets an HTTP-only cookie
that lasts 7 days. This is deliberately simple — no user
accounts, no OAuth, just a shared password for the
organizer.
Key subtlety: API routes like
/api/invitations/[token] are in the public bypass list
(because public invite pages need to GET them), but the
PATCH and DELETE handlers inside that route do a
manual cookie check:
const cookies = request.headers.get("cookie") || "";
if (!cookies.includes(
"lazy-inviter-auth=authenticated")) {
return Response.json(
{ error: "Unauthorized" }, { status: 401 });
}
This gives us fine-grained control: the same route can
serve both public GETs and authenticated
PATCH/DELETEs.
7. Routing & layouts (Next.js App Router)
Root layout
app/layout.tsx wraps every page. It loads three Google
Fonts (Cormorant Garamond, Oswald, Nunito) as CSS variables,
then renders the Navbar and Footer around {children}:
<html lang="de" className={`${cormorant.variable}
${oswald.variable} ${nunito.variable}`}>
<body>
<Navbar />
{children}
<Footer />
</body>
</html>
Server vs. Client components
Next.js defaults to server components — they run on the
server, can await database queries directly, and send only
HTML to the browser. Client components (marked with
"use client") run in the browser and can use React hooks.
The pattern in this project:
| Route | Server component | Client component |
|---|---|---|
/ (Dashboard) | page.tsx fetches invitations | Dashboard.tsx handles search/filter |
/project/[token] | page.tsx fetches all data | ProjectDetail.tsx handles editing |
/invite/[token] | page.tsx fetches invitation | InviteClient.tsx handles RSVP form |
/create | page.tsx (thin wrapper) | BirthdayWizard.tsx handles everything |
This "server component fetches, client component interacts" split is a common App Router pattern. The server component acts as a data gateway; the client component owns the UI state.
Dynamic route segments
[token] in a folder name is a dynamic segment.
Next.js 16 passes route params as a Promise that you must
await:
export default async function ProjectPage({
params,
}: {
params: Promise<{ token: string }>;
}) {
const { token } = await params;
// ... fetch data using token
}
8. Data-driven theming
The entire visual look of the app changes based on a single
Theme object. There are 8 themes defined in
app/lib/themes.ts:
interface Theme {
id: string; // "spiderman"
emoji: string; // "🕷️"
image?: string; // "/themes/spiderman.svg"
label: string; // "Spider-Man"
primary: string; // "#dc2626"
secondary: string; // "#1d4ed8"
accent: string; // "#fbbf24"
bg: string; // CSS gradient for page background
cardBg: string; // Card bg (often semi-transparent)
textColor: string; // Main text color
font: string; // CSS variable for the theme font
vibe: string; // Prompt hint for AI generation
}
Every component receives the theme prop and uses it to
set colours, fonts, gradients, and shadows. This is a
data-driven theming pattern — rather than CSS classes
or a theme provider, the theme object drives all styling
decisions directly through inline styles.
For example, a button gradient:
background: `linear-gradient(135deg,
${theme.primary}, ${theme.secondary})`
Four themes are classified as dark
(spiderman, dino, meerjungfrau, fussball). The
isDark boolean is derived from this list and passed down
to components that need to adjust text contrast:
export const DARK_THEME_IDS =
["spiderman", "dino", "meerjungfrau", "fussball"];
const isDark = DARK_THEME_IDS.includes(theme.id);
ThemeIcon — the adapter pattern
Some themes have a custom SVG illustration, others only
have an emoji. ThemeIcon abstracts this away:
export default function ThemeIcon({ theme, size }) {
const [imgError, setImgError] = useState(false);
if (theme.image && !imgError) {
return (
<img src={theme.image} alt={theme.label}
onError={() => setImgError(true)}
style={{ width: size, height: size }} />
);
}
return (
<span style={{ fontSize: size }}>
{theme.emoji}
</span>
);
}
If the image fails to load, it gracefully falls back to the emoji. This is the adapter pattern — one interface, two implementations behind the scenes.
types.ts — the type system
All TypeScript interfaces live in one file: Theme,
FormData, IdeaData, Invitation, Guest, Todo,
GuestInput, TodoInput, and InvitationWithCounts. This
is the single source of truth for data shapes across both
client and server code.
supabase.ts — two clients
export function createClient() { /* anon key */ }
export function createServiceClient() { /* service key */ }
Both are factory functions that validate environment variables before creating a client. The anon client is for public reads; the service client is for authenticated writes.
format.ts — date formatting
Two functions for German (Swiss) date formatting:
formatDate("2026-04-18")
// → "Samstag, 18. April 2026"
formatDateShort("2026-04-18")
// → "18. Apr."
The T00:00:00 suffix prevents timezone-related off-by-one
errors when parsing date-only strings:
new Date(dateStr + "T00:00:00")
Without this, new Date("2026-04-18") is interpreted as
UTC midnight, which can shift to the previous day in Swiss
time.
10. Reusable UI components
ui.tsx — the component kit
A single file exports 7 building blocks:
| Component | Purpose |
|---|---|
Card | Rounded container with theme-aware bg, shadow, blur |
SectionTitle | Themed <h2> with the theme's decorative font |
Field | Label + <input> combo for forms |
PrimaryButton | Gradient button using theme colours |
SecondaryButton | Subtle outline button, dark-mode aware |
IdeaSection | Titled list with coloured left-border items |
InfoPill | Small info card with icon, label, and text |
The Card component is the most used. It sets both
background and text colour based on the dark/light mode,
so all children inherit readable contrast:
<div style={{
background: theme.cardBg,
color: isDark ? "#e2e8f0" : "#1e293b",
borderRadius: "1.5rem",
backdropFilter: "blur(12px)",
}}>
EditableField.tsx — click-to-edit
Two components for inline editing:
-
EditableText— Click on any text to turn it into an<input>(or<textarea>ifmultiline). Press Enter to save, Escape to cancel, or click away (blur) to save. -
EditableList— For arrays like decorations or activities. Each item is a clickable row; clicking opens an inline input. Includes add (+) and delete (✕) buttons.
Both use color: inherit for the display mode, which
means they automatically match whatever text colour their
parent Card provides.
FloatingSymbols.tsx — decorative background
Creates 12 floating emoji/symbol elements that use the
CSS sparkle keyframe animation. They're positioned
pseudo-randomly using a mathematical trick:
left: `${(i * 137.5) % 100}%`,
top: `${(i * 97.3) % 100}%`,
The number 137.5 is close to the golden angle (137.508°), which produces an evenly-distributed but non-uniform spread — the same principle sunflowers use to arrange seeds.
ProgressBar.tsx — segmented bar
A reusable bar that takes an array of { color, value }
segments and renders them proportionally. Used for RSVP
status (green = accepted, yellow = maybe, grey = pending,
red = declined).
Navbar.tsx & Footer.tsx
The Navbar is a sticky glass-morphism header with:
- "🎈 Lazy Inviter" logo/home link
- "← Übersicht" breadcrumb on sub-pages
- "➕ Neue Einladung" button
Hidden on /login and /invite/* to keep the public
experience clean.
The Footer renders "© 2026 Rüchan Sixt. Built with Next.js & React." on every page.
11. The Birthday Wizard (creation flow)
BirthdayWizard.tsx is the largest component (~880 lines).
It guides the organizer through three steps:
Step 1: Party Details
A form card with:
- Theme picker — 4×2 grid of buttons, each showing
ThemeIcon+ label. Selecting a theme instantly changes the entire page's colours, font, and background. - Standard fields — child name, age, date, time, location, host name, phone, RSVP deadline, notes.
The entire background, card style, and input styles react
to the selected theme in real-time. This is driven by
computed style objects that reference theme.* properties:
const inputStyle = {
background: isDark
? "rgba(255,255,255,0.08)"
: "white",
color: theme.textColor,
border: `1.5px solid ${isDark
? "rgba(255,255,255,0.15)"
: "#e5e7eb"}`,
};
Step 2: AI-Generated Ideas (editable)
Clicking "Ideen generieren" calls /api/generate, which
sends the party details to Claude and returns a JSON object
with tagline, invitation text, decorations, activities,
food idea, and dress code.
All generated content is editable. The wizard stores
two copies — ideas (original from AI) and editedIdeas
(the user's modified version). EditableText and
EditableList components make every field click-to-edit.
There's also a fallback mechanism: if the AI call fails, the wizard shows a hardcoded template populated with the theme name.
Step 3: Preview, Guests & Save
Two tabs:
- 📱 Digital / RSVP — shows the
InvitationCardcomponent (the invitation as guests will see it). - 🖨️ Print — shows
PrintInvitationwith print and PDF export buttons.
Below the preview: a GuestListManager for adding guests
with phone numbers, and a "Save & Share" section. Saving
calls POST /api/invitations, which generates a nanoid
token and returns a shareable URL. After saving, a
"📋 Zum Projekt →" button navigates to the project detail
view.
12. AI idea generation (Claude API)
/api/generate/route.ts
This endpoint calls the Anthropic Messages API directly
via fetch (no SDK):
const res = await fetch(
"https://api.anthropic.com/v1/messages",
{
method: "POST",
headers: {
"x-api-key": apiKey,
"anthropic-version": "2023-06-01",
},
body: JSON.stringify({
model: "claude-sonnet-4-20250514",
max_tokens: 1000,
tools: [{
type: "web_search_20250305",
name: "web_search",
}],
messages: [
{ role: "user", content: prompt },
],
}),
}
);
The prompt asks Claude to respond with a JSON object containing tagline, invitation text, decorations, activities, food idea, and dress code — all in German, tailored to the Swiss context.
Web search is enabled as a tool, allowing Claude to look up current party trends for the specific theme.
The response is parsed by extracting the first {...}
block from the text content with a regex:
const match = textContent.match(/\{[\s\S]*\}/);
const ideas = JSON.parse(match[0]);
Rate limiting
An in-memory Map tracks requests per IP address, allowing
10 requests per hour. This is a sliding window approach
— simple and effective for a single-server deployment:
const rateLimit = new Map<string,
{ count: number; resetAt: number }>();
13. The Organizer Dashboard
Server component: app/page.tsx
Fetches all invitations with createServiceClient() and
enriches each one with guest status counts (accepted,
maybe, pending, declined). Passes the result to
<Dashboard />.
Client component: Dashboard.tsx
The dashboard provides three key features:
1. Search — A text input filters invitations by child name, date string, or theme label (e.g. typing "Luna" or "Spider" narrows the list).
2. Filter tabs — Three toggle buttons:
- Aktiv — invitations where
archived === false - Archiv — invitations where
archived === true - Alle — no filter
Each tab shows its count as a small badge.
3. Invitation cards — Each card shows:
- Theme icon + child name + age
- Party date
- RSVP progress bar (green/yellow/grey/red segments)
- RSVP summary text ("3 zugesagt · 1 vielleicht")
- Action buttons: Archive/Restore + Delete (with confirmation dialog)
Clicking a card navigates to /project/[token].
Archive sends PATCH { archived: true/false } and
updates state optimistically. Delete shows a "Sicher?"
confirmation inline, then sends DELETE and removes the
card from state.
All text and action button colours adapt to dark themes
using the isDark flag.
14. Project Detail (editing & management)
Server component: app/project/[token]/page.tsx
Fetches the invitation, guests, and todos from Supabase.
Resolves the theme. Passes everything to
<ProjectDetail />.
Client component: ProjectDetail.tsx
A single scrollable page with these sections:
📋 Details — Inline-editable fields for child name,
age, date, time, location, host, and contact. Every field
is an EditableText — click to edit, blur or Enter to
save.
✨ Einladungstext & Ideen — Editable tagline, invitation text (multiline), decoration list, activity list, food idea, and dress code.
✉️ Einladung — Collapsible invitation preview with print and PDF buttons.
📊 RSVP Status — RsvpStatusTable showing
colour-coded badges, progress bar, and per-guest rows
with response dates.
📋 Gästeliste — GuestListManager for adding/removing
guests.
✅ To-Do Liste — TodoList with manual add, AI
generation, and checkbox toggle.
Share Section — QR code + copy link.
Change detection and saving
The component tracks whether form data, ideas, or the guest list has changed from the original:
const hasChanges =
JSON.stringify(editForm) !== JSON.stringify(form) ||
JSON.stringify(editIdeas) !== JSON.stringify(ideas) ||
JSON.stringify(guests) !== JSON.stringify(
initialGuests.map(g =>
({ name: g.name, phone: g.phone }))
);
When hasChanges is true, a "💾 Änderungen speichern"
button appears. Clicking it sends a PATCH request with
all modified data.
15. Public invitation & RSVP
The public invite page (/invite/[token])
This is the guest-facing page — no authentication
required. The server component fetches the invitation
data, and InviteClient.tsx renders:
- InvitationCard — the themed invitation with all party details.
- RSVP form — phone number input + three status buttons (Ja / Leider nicht / Vielleicht) + submit.
Phone-based RSVP matching
The RSVP API (/api/rsvp) uses phone number
normalisation to match guests:
function normalizePhone(phone: string): string {
const digits = phone.replace(/\D/g, "");
if (digits.startsWith("41") && digits.length > 9)
return digits.slice(-9);
if (digits.startsWith("0") && digits.length === 10)
return digits.slice(1);
return digits.slice(-9);
}
This means a guest can type "079 123 45 67", "+41 79 123 45 67", or "0791234567" and they will all match the same record. The last 9 digits are the canonical form for Swiss mobile numbers.
16. To-do lists with AI suggestions
TodoList.tsx
The to-do list lives inside ProjectDetail and supports:
- Manual add — text input + optional date picker + "+" button.
- AI generation — calls
/api/todos/generate, which asks Claude for 8–10 preparation tasks with realistic due dates (e.g. "Torte bestellen" 7 days before, "Luftballons kaufen" 2 days before). - Optimistic toggle — checking/unchecking a todo updates the UI immediately, then sends the PATCH request. If it fails, the UI reverts:
const toggleTodo = async (id, completed) => {
// Optimistic: update UI now
setTodos(prev =>
prev.map(t =>
t.id === id ? { ...t, completed } : t));
try {
await fetch(`/api/todos/${id}`, {
method: "PATCH",
body: JSON.stringify({ completed }),
});
} catch {
// Revert if server fails
setTodos(prev =>
prev.map(t =>
t.id === id
? { ...t, completed: !completed }
: t));
}
};
- Overdue highlighting — tasks past their due date get a red badge.
- Sorting — uncompleted first, then by due date, then completed at the bottom.
AI todo generation (/api/todos/generate)
The prompt asks Claude for tasks in Swiss context (mentioning Migros, Coop, local suppliers). It includes a fallback function that generates 8 generic tasks if the API call fails — the user always gets suggestions.
17. PDF export & printing
The DOM → Canvas → PDF pipeline
Since browser "Save as PDF" ignores many inline styles, the app uses a client-side pipeline:
- html2canvas-pro captures the
PrintInvitationcomponent as a canvas bitmap. - jsPDF creates an A5-sized PDF and places the image inside it.
Both libraries are dynamically imported — they're only loaded when the user clicks "Als PDF exportieren", keeping the initial bundle small:
const [{ default: html2canvas }, { jsPDF }] =
await Promise.all([
import("html2canvas-pro"),
import("jspdf"),
]);
The PrintInvitation component is rendered off-screen
(position: absolute; left: -9999px) so html2canvas can
capture it without the user seeing the rendering process.
Print styles
globals.css includes @media print rules that hide
non-essential elements (navbar, buttons, floating symbols)
and optimise the layout for A5 paper.
18. QR code & sharing
ShareSection.tsx uses the qrcode.react library to
generate an SVG QR code coloured with the theme's primary
colour:
<QRCodeSVG value={url} size={180}
fgColor={theme.primary} level="M" />
It also provides:
- Copy link — uses
navigator.clipboard.writeText(). - Download QR — converts the SVG to PNG via a canvas:
serialise →
<img>→ draw to canvas →toDataURL()→ trigger download.
19. Guest reconciliation on edit
When the organizer edits the guest list in ProjectDetail,
the PATCH /api/invitations/[token] handler must be smart
about preserving existing RSVP responses. It uses
phone number matching to reconcile:
- Fetch existing guests from the database.
- Build a
Mapof existing guests keyed by normalised phone number. - Compare with the new guest list:
- Existing phone → update name if changed, keep RSVP status untouched.
- New phone → insert with
status: "pending". - Removed phone → delete the guest record.
This ensures that if a parent has already RSVP'd, their response isn't lost when the organizer edits the guest list.
20. Dark theme support
Four of the eight themes have dark backgrounds. The
isDark boolean is threaded through the entire component
tree and affects:
| Concern | Light mode | Dark mode |
|---|---|---|
| Card text colour | #1e293b | #e2e8f0 |
| Input background | white | rgba(255,255,255,0.08) |
| Input border | #e5e7eb | rgba(255,255,255,0.15) |
| Label colour | #374151 | #94a3b8 |
| Muted text | #9ca3af | #94a3b8 |
| Card shadow | rgba(0,0,0,0.07) | rgba(0,0,0,0.4) |
The Card component in ui.tsx sets the base text
colour, so child components using color: inherit
automatically get the right contrast.
On the Dashboard, each invitation card independently
determines isDark based on its own theme — so a
Spider-Man card (dark) and an Einhorn card (light) can
appear side by side with correct contrast.
21. How to replicate this project from scratch
If you wanted to build this yourself, here's the recommended order:
-
Scaffold —
npx create-next-app@latest lazy-inviterwith TypeScript and App Router. -
Set up Supabase — Create a project, run
supabase/schema.sqlin the SQL editor to create tables and RLS policies. -
Environment — Copy
.env.exampleto.env.localand fill in Supabase URL/keys, an Anthropic API key, and an app password. -
Supabase clients — Create
app/lib/supabase.tswith the two-client pattern (anon for reads, service for writes). -
Type system — Define all interfaces in
app/lib/types.ts. This is your contract between front and back end. -
Theme definitions — Build the
themes.tsarray with colours, fonts, and thevibefield for AI prompts. -
Auth — Implement
proxy.tsfor route protection andapp/api/login/route.tsfor cookie-based login. -
Login page — Simple form at
app/login/page.tsx. -
Font setup — Create
app/fonts.tsto load Google Fonts withnext/font, expose as CSS variables. -
UI kit — Build
ui.tsxwith Card, Field, Button components. These will be used everywhere. -
Birthday Wizard — The creation flow. Start with the form (Step 1), then the AI generation (Step 2), then the preview + save (Step 3). Build components as you go:
InvitationCard,PrintInvitation,FloatingSymbols,StepIndicator,EditableField. -
API routes —
POST /api/generate(Claude AI),POST /api/invitations(save),GET/PATCH/DELETEfor individual invitations. -
Public invite —
/invite/[token]page withInviteClientand the RSVP form.POST /api/rsvpfor phone-based matching. -
Dashboard — Replace the home page with a server component that fetches all invitations, and a client
Dashboardcomponent with search, filter tabs, and archive/delete actions. -
Project Detail —
/project/[token]with inline editing, RSVP table, guest management. -
To-do lists —
todostable, CRUD API routes, AI generation endpoint, and theTodoListcomponent with optimistic updates. -
PDF export — Add
html2canvas-pro+jspdfwith dynamic imports. Render the print layout off-screen and capture it. -
QR code & sharing — Add
qrcode.react, build theShareSectionwith copy link and QR download. -
Dark theme polish — Go through every component and ensure
isDarkbranches provide readable contrast. -
Navbar & Footer — Add persistent navigation and copyright footer in the root layout.
Generated March 2026 — covers commit state as of the documentation date.