Tutorial-like documentation of Lazy-Inviter App

learningreactdevelopmentdocs

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

  1. What is this project?
  2. Tech stack at a glance
  3. Project structure
  4. Environment & configuration
  5. Database schema (Supabase)
  6. Authentication — proxy.ts & login
  7. Routing & layouts (Next.js App Router)
  8. Data-driven theming
  9. Shared library code
  10. Reusable UI components
  11. The Birthday Wizard (creation flow)
  12. AI idea generation (Claude API)
  13. The Organizer Dashboard
  14. Project Detail (editing & management)
  15. Public invitation & RSVP
  16. To-do lists with AI suggestions
  17. PDF export & printing
  18. QR code & sharing
  19. Guest reconciliation on edit
  20. Dark theme support
  21. 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

TechnologyWhat it does hereLearn more
Next.js 16 (App Router)Framework — file-based routing, server components, API routes📖 Next.js docs
React 19UI library — components, hooks, state management📖 React docs
TypeScript 5Type 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 + jsPDFClient-side DOM → Canvas → PDF export📖 jsPDF docs
qrcode.reactQR code SVG generation for shareable links📖 qrcode.react
nanoidGenerates short, URL-safe unique tokens for invitations📖 nanoid
Tailwind CSS 4Minimal 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.

📖 App Router file conventions


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.

📖 Supabase Row Level Security


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.

📖 Next.js Middleware


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:

RouteServer componentClient component
/ (Dashboard)page.tsx fetches invitationsDashboard.tsx handles search/filter
/project/[token]page.tsx fetches all dataProjectDetail.tsx handles editing
/invite/[token]page.tsx fetches invitationInviteClient.tsx handles RSVP form
/createpage.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.

📖 Server Components

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
}

📖 Dynamic routes


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.

📖 Adapter pattern


9. Shared library code

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.

📖 Date.toLocaleDateString()


10. Reusable UI components

ui.tsx — the component kit

A single file exports 7 building blocks:

ComponentPurpose
CardRounded container with theme-aware bg, shadow, blur
SectionTitleThemed <h2> with the theme's decorative font
FieldLabel + <input> combo for forms
PrimaryButtonGradient button using theme colours
SecondaryButtonSubtle outline button, dark-mode aware
IdeaSectionTitled list with coloured left-border items
InfoPillSmall 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> if multiline). 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.

📖 Golden angle

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 InvitationCard component (the invitation as guests will see it).
  • 🖨️ Print — shows PrintInvitation with 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.

📖 useState hook


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 }>();

📖 Anthropic Messages API


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 StatusRsvpStatusTable showing colour-coded badges, progress bar, and per-guest rows with response dates.

📋 GästelisteGuestListManager for adding/removing guests.

✅ To-Do ListeTodoList 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:

  1. InvitationCard — the themed invitation with all party details.
  2. 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.

📖 String.replace()


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.

📖 Optimistic updates


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:

  1. html2canvas-pro captures the PrintInvitation component as a canvas bitmap.
  2. 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.

📖 Dynamic imports


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.

📖 Clipboard API


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:

  1. Fetch existing guests from the database.
  2. Build a Map of existing guests keyed by normalised phone number.
  3. 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:

ConcernLight modeDark mode
Card text colour#1e293b#e2e8f0
Input backgroundwhitergba(255,255,255,0.08)
Input border#e5e7ebrgba(255,255,255,0.15)
Label colour#374151#94a3b8
Muted text#9ca3af#94a3b8
Card shadowrgba(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:

  1. Scaffoldnpx create-next-app@latest lazy-inviter with TypeScript and App Router.

  2. Set up Supabase — Create a project, run supabase/schema.sql in the SQL editor to create tables and RLS policies.

  3. Environment — Copy .env.example to .env.local and fill in Supabase URL/keys, an Anthropic API key, and an app password.

  4. Supabase clients — Create app/lib/supabase.ts with the two-client pattern (anon for reads, service for writes).

  5. Type system — Define all interfaces in app/lib/types.ts. This is your contract between front and back end.

  6. Theme definitions — Build the themes.ts array with colours, fonts, and the vibe field for AI prompts.

  7. Auth — Implement proxy.ts for route protection and app/api/login/route.ts for cookie-based login.

  8. Login page — Simple form at app/login/page.tsx.

  9. Font setup — Create app/fonts.ts to load Google Fonts with next/font, expose as CSS variables.

  10. UI kit — Build ui.tsx with Card, Field, Button components. These will be used everywhere.

  11. 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.

  12. API routesPOST /api/generate (Claude AI), POST /api/invitations (save), GET/PATCH/DELETE for individual invitations.

  13. Public invite/invite/[token] page with InviteClient and the RSVP form. POST /api/rsvp for phone-based matching.

  14. Dashboard — Replace the home page with a server component that fetches all invitations, and a client Dashboard component with search, filter tabs, and archive/delete actions.

  15. Project Detail/project/[token] with inline editing, RSVP table, guest management.

  16. To-do liststodos table, CRUD API routes, AI generation endpoint, and the TodoList component with optimistic updates.

  17. PDF export — Add html2canvas-pro + jspdf with dynamic imports. Render the print layout off-screen and capture it.

  18. QR code & sharing — Add qrcode.react, build the ShareSection with copy link and QR download.

  19. Dark theme polish — Go through every component and ensure isDark branches provide readable contrast.

  20. Navbar & Footer — Add persistent navigation and copyright footer in the root layout.


Generated March 2026 — covers commit state as of the documentation date.