From e02310d84e61ad540492a4af4e9ee7d0b26819e1 Mon Sep 17 00:00:00 2001 From: Thomas Tschinkel Date: Sun, 14 Jun 2026 16:59:33 +0200 Subject: [PATCH 1/8] docs: add first-login quickstart guide design spec --- .../2026-06-14-quickstart-guide-design.md | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-14-quickstart-guide-design.md diff --git a/docs/superpowers/specs/2026-06-14-quickstart-guide-design.md b/docs/superpowers/specs/2026-06-14-quickstart-guide-design.md new file mode 100644 index 0000000..7513544 --- /dev/null +++ b/docs/superpowers/specs/2026-06-14-quickstart-guide-design.md @@ -0,0 +1,98 @@ +# First-login Quickstart Guide — Design + +**Date:** 2026-06-14 +**Status:** Approved + +## Overview + +A one-time welcome modal that appears the first time a user lands on the +Dashboard after signing up. It walks through a short sequence of slides covering +what OpenThorn is, where to connect a provider, where Templates live, how to open +the Restaurant Landing template, and the core build/deploy flow. Each feature +slide carries an **action button** that closes the modal and navigates the user +to the relevant place. The guide is shown once per account, tracked via a flag on +the `profiles` table. + +The existing inline "Launch checklist" card on the Dashboard +(`DashboardPage.tsx`) stays as the ongoing reference; the quickstart guide +complements it rather than replacing it. + +## Show-once mechanism (database) + +New migration `supabase/migrations/_add_quickstart_flag.sql`: + +```sql +alter table public.profiles + add column if not exists has_seen_quickstart boolean not null default false; + +-- Existing users have already used the app — don't re-onboard them. +-- Only accounts created after this migration get the default `false`. +update public.profiles set has_seen_quickstart = true; +``` + +- New signups receive `has_seen_quickstart = false` from the column default via + the existing `handle_new_user` trigger. Only these users see the guide. +- The existing `profiles_update_own` RLS policy already allows a user to update + their own row, so the client can flip the flag itself. No policy changes. + +### Read / dismiss flow (DashboardPage) + +- On mount, once `user` is available, query + `select has_seen_quickstart from profiles where id = user.id`. +- If the value is `false`, open the quickstart modal. +- On dismiss (finishing the last slide, closing, or pressing any action button), + optimistically hide the modal in local state and run + `update profiles set has_seen_quickstart = true where id = user.id`. +- Failures to persist are logged via the existing `logError` helper and do not + block the UI (worst case: the guide could reappear on next load). + +## Component + +New `src/components/QuickstartGuide/QuickstartGuide.tsx` with a co-located +`QuickstartGuide.module.css`, following the existing modal patterns in the +codebase (backdrop + centered card, like the Publish-to-Community modal in +`DashboardPage.tsx` and the Templates preview overlay): click-outside / Escape to +close, design-token colors from `src/index.css`. + +**Props:** +- `firstName: string` +- `onClose: () => void` — called for both "finish" and "close"; the parent flips + the DB flag here. + +The component owns its own slide state (`step`, prev/next handlers, progress +dots). It receives a `navigate`-style callback (or imports `useNavigate`) so +action buttons can route; each action button calls `onClose` (which persists the +flag) before navigating, so the guide never reappears. + +**Slides** (each: a small inline SVG illustration, heading, one or two lines of +copy, and an action button): + +1. **Welcome, {firstName}** — OpenThorn builds complete websites from a prompt + using your own AI provider key. → *Next* +2. **Connect a provider** — Your key stays yours (BYOK). Find it under + **Providers** in the sidebar. → *Go to Providers* — `navigate('/providers')` +3. **Browse Templates** — Production-ready starting points live under + **Templates**. → *Open Templates* — `navigate('/templates')` +4. **Try the Restaurant Landing template** — Open Templates, click a card to + preview it, then "Use this template." → *Open Restaurant template* — + `navigate('/templates', { state: { openTemplateId: 'restaurant-landing' } })` +5. **Build & deploy** — Describe your idea in the prompt box and deploy when + ready. → *Get started* (closes the guide) + +The guide is rendered by `DashboardPage` as a sibling of the existing modals, +gated on the fetched `has_seen_quickstart` flag being `false`. + +## Supporting change: TemplatesPage deep-link + +`TemplatesPage` reads `location.state.openTemplateId` on mount and, if present and +matching a known template, auto-opens that template's preview overlay +(`setSelected(...)`). This lets slide 4's action button land the user directly on +the Restaurant Landing preview. The Restaurant template id is `restaurant-landing` +(verified in `src/lib/templates.ts`). + +## Out of scope (YAGNI) + +- No guided spotlight/coachmark tour over real UI elements. +- No re-show mechanism or "view guide again" entry point. +- No admin toggle or per-tenant configuration. +- No change to the existing inline Launch checklist. From a4e3ccfcbb906e0a7c823b00df0d729649b0496c Mon Sep 17 00:00:00 2001 From: Thomas Tschinkel Date: Sun, 14 Jun 2026 17:02:26 +0200 Subject: [PATCH 2/8] docs: add first-login quickstart guide implementation plan --- .../plans/2026-06-14-quickstart-guide.md | 656 ++++++++++++++++++ 1 file changed, 656 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-14-quickstart-guide.md diff --git a/docs/superpowers/plans/2026-06-14-quickstart-guide.md b/docs/superpowers/plans/2026-06-14-quickstart-guide.md new file mode 100644 index 0000000..f9c78a0 --- /dev/null +++ b/docs/superpowers/plans/2026-06-14-quickstart-guide.md @@ -0,0 +1,656 @@ +# First-login Quickstart Guide Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Show a one-time, 5-slide welcome modal to brand-new users on their first Dashboard visit, pointing them to Providers, Templates, the Restaurant Landing template, and the build/deploy flow. + +**Architecture:** A DB flag `profiles.has_seen_quickstart` gates the modal (existing users backfilled to `true`). DashboardPage fetches the flag, renders a new `QuickstartGuide` component when it is `false`, and flips it to `true` on dismiss. Pure slide config + show-once predicate live in a testable `src/lib/quickstart.ts` module. A small deep-link addition to TemplatesPage lets the Restaurant slide open that template's preview directly. + +**Tech Stack:** React 19 + TypeScript, React Router v7, Supabase (Postgres + RLS), CSS Modules, Vitest (node env). + +--- + +## File Structure + +- Create: `supabase/migrations/20260614010000_add_quickstart_flag.sql` — add + backfill the flag. +- Create: `src/lib/quickstart.ts` — slide config + `shouldShowQuickstart` predicate (pure, testable). +- Create: `src/lib/__tests__/quickstart.test.ts` — unit tests for the above. +- Create: `src/components/QuickstartGuide/QuickstartGuide.tsx` — the modal component. +- Create: `src/components/QuickstartGuide/QuickstartGuide.module.css` — modal styles. +- Modify: `src/pages/DashboardPage.tsx` — fetch flag, render guide, persist dismissal. +- Modify: `src/pages/TemplatesPage.tsx` — auto-open a template from `location.state.openTemplateId`. + +--- + +## Task 1: Database migration for the show-once flag + +**Files:** +- Create: `supabase/migrations/20260614010000_add_quickstart_flag.sql` + +- [ ] **Step 1: Write the migration** + +Create `supabase/migrations/20260614010000_add_quickstart_flag.sql`: + +```sql +-- First-login quickstart guide: track whether a user has seen it. +alter table public.profiles + add column if not exists has_seen_quickstart boolean not null default false; + +-- Existing users have already used the app — don't re-onboard them. +-- New signups created after this migration get the default `false`. +update public.profiles set has_seen_quickstart = true; +``` + +- [ ] **Step 2: Verify it is valid SQL by inspection** + +The existing `profiles_update_own` RLS policy (in +`20260603000000_profiles_and_collaboration.sql`) already permits a user to update +their own row, so no new policy is required. Confirm no other migration adds a +`has_seen_quickstart` column (it does not). + +- [ ] **Step 3: Commit** + +```bash +git add supabase/migrations/20260614010000_add_quickstart_flag.sql +git commit -m "feat(db): add has_seen_quickstart flag to profiles" +``` + +> **Note for the implementer:** This migration must be applied to the live +> database with `supabase db push` (or via the dashboard) before the feature +> works end-to-end. Applying it is a deploy step, not part of the code commit. + +--- + +## Task 2: Pure quickstart logic and slide config + +**Files:** +- Create: `src/lib/quickstart.ts` +- Test: `src/lib/__tests__/quickstart.test.ts` + +- [ ] **Step 1: Write the failing test** + +Create `src/lib/__tests__/quickstart.test.ts`: + +```ts +import { describe, it, expect } from 'vitest' +import { QUICKSTART_SLIDES, shouldShowQuickstart } from '../quickstart' + +describe('shouldShowQuickstart', () => { + it('shows only when the flag is explicitly false', () => { + expect(shouldShowQuickstart(false)).toBe(true) + }) + it('does not show when already seen', () => { + expect(shouldShowQuickstart(true)).toBe(false) + }) + it('does not show while unknown/loading (null or undefined)', () => { + expect(shouldShowQuickstart(null)).toBe(false) + expect(shouldShowQuickstart(undefined)).toBe(false) + }) +}) + +describe('QUICKSTART_SLIDES', () => { + it('starts with an advance action and ends with a finish action', () => { + expect(QUICKSTART_SLIDES[0].action.type).toBe('advance') + expect(QUICKSTART_SLIDES[QUICKSTART_SLIDES.length - 1].action.type).toBe('finish') + }) + it('routes the Providers slide to /providers', () => { + const slide = QUICKSTART_SLIDES.find((s) => s.id === 'providers') + expect(slide?.action).toEqual({ type: 'navigate', label: 'Go to Providers', to: '/providers' }) + }) + it('deep-links the Restaurant slide to the restaurant-landing template', () => { + const slide = QUICKSTART_SLIDES.find((s) => s.id === 'restaurant') + expect(slide?.action).toMatchObject({ + type: 'navigate', + to: '/templates', + state: { openTemplateId: 'restaurant-landing' }, + }) + }) + it('has unique slide ids', () => { + const ids = QUICKSTART_SLIDES.map((s) => s.id) + expect(new Set(ids).size).toBe(ids.length) + }) +}) +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `npx vitest run src/lib/__tests__/quickstart.test.ts` +Expected: FAIL — cannot resolve module `../quickstart`. + +- [ ] **Step 3: Write the implementation** + +Create `src/lib/quickstart.ts`: + +```ts +/** Action taken when a quickstart slide's primary button is pressed. */ +export type QuickstartAction = + | { type: 'advance'; label: string } + | { type: 'finish'; label: string } + | { type: 'navigate'; label: string; to: string; state?: Record } + +export interface QuickstartSlide { + id: string + heading: string + body: string + action: QuickstartAction +} + +/** + * Slides shown to a brand-new user on their first dashboard visit. + * `navigate` actions close the guide (persisting the flag) and route the user. + */ +export const QUICKSTART_SLIDES: QuickstartSlide[] = [ + { + id: 'welcome', + heading: 'Welcome to OpenThorn', + body: 'OpenThorn builds complete websites from a single prompt — using your own AI provider key.', + action: { type: 'advance', label: 'Next' }, + }, + { + id: 'providers', + heading: 'Connect a provider', + body: 'Your API key stays yours (BYOK). Add it under Providers in the sidebar to start generating.', + action: { type: 'navigate', label: 'Go to Providers', to: '/providers' }, + }, + { + id: 'templates', + heading: 'Browse Templates', + body: 'Prefer a head start? Production-ready starting points live under Templates.', + action: { type: 'navigate', label: 'Open Templates', to: '/templates' }, + }, + { + id: 'restaurant', + heading: 'Try the Restaurant Landing template', + body: 'Open Templates, click a card to preview it, then “Use this template” to customize it with AI.', + action: { + type: 'navigate', + label: 'Open Restaurant template', + to: '/templates', + state: { openTemplateId: 'restaurant-landing' }, + }, + }, + { + id: 'build', + heading: 'Build & deploy', + body: 'Describe your idea in the prompt box on the dashboard, then deploy your site when it’s ready.', + action: { type: 'finish', label: 'Get started' }, + }, +] + +/** Show the guide only when the persisted flag is explicitly false. */ +export function shouldShowQuickstart(hasSeen: boolean | null | undefined): boolean { + return hasSeen === false +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `npx vitest run src/lib/__tests__/quickstart.test.ts` +Expected: PASS (9 assertions across the two describe blocks). + +- [ ] **Step 5: Commit** + +```bash +git add src/lib/quickstart.ts src/lib/__tests__/quickstart.test.ts +git commit -m "feat: add quickstart slide config and show-once predicate" +``` + +--- + +## Task 3: QuickstartGuide component + +**Files:** +- Create: `src/components/QuickstartGuide/QuickstartGuide.tsx` +- Create: `src/components/QuickstartGuide/QuickstartGuide.module.css` + +- [ ] **Step 1: Write the component** + +Create `src/components/QuickstartGuide/QuickstartGuide.tsx`: + +```tsx +import { useState, useEffect, useCallback } from 'react' +import { useNavigate } from 'react-router-dom' +import { QUICKSTART_SLIDES } from '../../lib/quickstart' +import styles from './QuickstartGuide.module.css' + +interface QuickstartGuideProps { + firstName: string + /** Called whenever the guide is dismissed (finish, navigate, or close). The + * parent persists the has_seen_quickstart flag here. */ + onClose: () => void +} + +export default function QuickstartGuide({ firstName, onClose }: QuickstartGuideProps) { + const navigate = useNavigate() + const [step, setStep] = useState(0) + const slide = QUICKSTART_SLIDES[step] + const isFirst = step === 0 + const total = QUICKSTART_SLIDES.length + + const handleAction = useCallback(() => { + const action = slide.action + if (action.type === 'advance') { + setStep((s) => Math.min(s + 1, total - 1)) + return + } + // Both 'finish' and 'navigate' dismiss the guide. + onClose() + if (action.type === 'navigate') { + navigate(action.to, action.state ? { state: action.state } : undefined) + } + }, [slide, onClose, navigate, total]) + + // Escape closes the guide. + useEffect(() => { + const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose() } + document.addEventListener('keydown', handler) + return () => document.removeEventListener('keydown', handler) + }, [onClose]) + + const heading = slide.id === 'welcome' + ? `Welcome to OpenThorn, ${firstName}` + : slide.heading + + return ( +
{ if (e.target === e.currentTarget) onClose() }} + role="dialog" + aria-modal="true" + aria-label="Quickstart guide" + > +
+ + + Getting started · {step + 1}/{total} +

{heading}

+

{slide.body}

+ + + +
+ {!isFirst && ( + + )} + +
+ + +
+
+ ) +} +``` + +- [ ] **Step 2: Write the styles** + +Create `src/components/QuickstartGuide/QuickstartGuide.module.css`. Use design +tokens from `src/index.css` (`--color-bg`, `--color-text`, `--color-accent`); do +not hardcode brand hex values: + +```css +.backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.55); + backdrop-filter: blur(4px); + z-index: 300; + display: grid; + place-items: center; + padding: 1.5rem; + animation: pageFade 0.2s ease both; +} + +.modal { + position: relative; + width: 100%; + max-width: 460px; + background: var(--color-bg); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 20px; + padding: 2.25rem 2rem 1.5rem; + box-shadow: 0 24px 80px rgba(0, 0, 0, 0.5); + text-align: center; + animation: pageRise 0.25s ease both; +} + +.close { + position: absolute; + top: 1rem; + right: 1rem; + width: 32px; + height: 32px; + display: grid; + place-items: center; + border-radius: 8px; + color: var(--color-text); + opacity: 0.6; + transition: opacity 0.15s, background 0.15s; +} +.close:hover { opacity: 1; background: rgba(255, 255, 255, 0.08); } + +.eyebrow { + display: inline-block; + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--color-accent); + margin-bottom: 0.85rem; +} + +.heading { + font-size: 1.5rem; + font-weight: 700; + letter-spacing: -0.02em; + margin-bottom: 0.75rem; + color: var(--color-text); +} + +.body { + font-size: 0.95rem; + line-height: 1.65; + color: var(--color-text); + opacity: 0.75; + margin: 0 auto 1.5rem; + max-width: 36ch; +} + +.dots { + display: flex; + justify-content: center; + gap: 0.4rem; + margin-bottom: 1.5rem; +} +.dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.2); + transition: background 0.2s, transform 0.2s; +} +.dotActive { background: var(--color-accent); transform: scale(1.25); } + +.actions { + display: flex; + gap: 0.75rem; + justify-content: center; +} + +.primary { + flex: 1; + max-width: 280px; + padding: 0.8rem 1.5rem; + background: var(--color-accent); + color: #fff; + border-radius: 12px; + font-weight: 600; + font-size: 0.95rem; + transition: opacity 0.15s, transform 0.15s; +} +.primary:hover { opacity: 0.9; transform: translateY(-1px); } + +.back { + padding: 0.8rem 1.25rem; + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 12px; + color: var(--color-text); + font-weight: 500; + font-size: 0.95rem; + transition: border-color 0.15s; +} +.back:hover { border-color: var(--color-accent); } + +.skip { + margin-top: 1rem; + font-size: 0.8rem; + color: var(--color-text); + opacity: 0.5; + transition: opacity 0.15s; +} +.skip:hover { opacity: 0.85; } +``` + +> **Note:** `pageFade` and `pageRise` keyframes are defined globally in +> `src/index.css` (per CLAUDE.md). If a lint/build error reports them missing, +> replace the `animation` lines with a local `@keyframes` fallback, but they +> should resolve globally. + +- [ ] **Step 3: Verify it compiles** + +Run: `npm run build` +Expected: `tsc -b && vite build` completes with no errors referencing +`QuickstartGuide`. + +- [ ] **Step 4: Commit** + +```bash +git add src/components/QuickstartGuide/QuickstartGuide.tsx src/components/QuickstartGuide/QuickstartGuide.module.css +git commit -m "feat: add QuickstartGuide modal component" +``` + +--- + +## Task 4: Wire the guide into DashboardPage + +**Files:** +- Modify: `src/pages/DashboardPage.tsx` + +- [ ] **Step 1: Add the import** + +Near the other component imports (after the `PromptInput` import around +`src/pages/DashboardPage.tsx:9`), add: + +```tsx +import QuickstartGuide from '../components/QuickstartGuide/QuickstartGuide' +import { shouldShowQuickstart } from '../lib/quickstart' +``` + +- [ ] **Step 2: Add state for the guide** + +After the `const [sidebarOpen, setSidebarOpen] = useState(false)` line +(`src/pages/DashboardPage.tsx:110`), add: + +```tsx + const [showQuickstart, setShowQuickstart] = useState(false) +``` + +- [ ] **Step 3: Fetch the flag on mount** + +Add a new effect immediately after the provider-status effect (which ends near +`src/pages/DashboardPage.tsx:286`). Insert: + +```tsx + // First-login quickstart guide — show once per account. + useEffect(() => { + if (!user) return + let cancelled = false + supabase + .from('profiles') + .select('has_seen_quickstart') + .eq('id', user.id) + .single() + .then(({ data, error }) => { + if (error) { + logError('DashboardQuickstartFlag', error) + return + } + if (!cancelled && shouldShowQuickstart(data?.has_seen_quickstart)) { + setShowQuickstart(true) + } + }) + return () => { cancelled = true } + }, [user]) +``` + +- [ ] **Step 4: Add the dismiss handler** + +Add this `useCallback` alongside the other handlers (e.g. after +`handleExampleClick` near `src/pages/DashboardPage.tsx:507`): + +```tsx + const handleQuickstartClose = useCallback(async () => { + setShowQuickstart(false) + if (!user) return + const { error } = await supabase + .from('profiles') + .update({ has_seen_quickstart: true }) + .eq('id', user.id) + if (error) logError('DashboardQuickstartDismiss', error) + }, [user]) +``` + +- [ ] **Step 5: Render the guide** + +Inside the top-level `<>...` return, render the guide as a sibling of the +other modals — directly after the opening `<>` is fine, but to match the +existing "modals live after `` root" pattern, add it right before the +Publish modal block (`{publishingProject && (` near +`src/pages/DashboardPage.tsx:1033`): + +```tsx + {showQuickstart && ( + + )} + +``` + +(`firstName` is already computed at `src/pages/DashboardPage.tsx:124`.) + +- [ ] **Step 6: Verify build and lint** + +Run: `npm run build && npm run lint` +Expected: no errors. (`useCallback` and `useEffect` are already imported at +`src/pages/DashboardPage.tsx:1`.) + +- [ ] **Step 7: Commit** + +```bash +git add src/pages/DashboardPage.tsx +git commit -m "feat: show quickstart guide to first-login users on dashboard" +``` + +--- + +## Task 5: TemplatesPage deep-link to a template + +**Files:** +- Modify: `src/pages/TemplatesPage.tsx` + +- [ ] **Step 1: Read the route state** + +`useNavigate` is already imported. Add `useLocation` to the existing +`react-router-dom` import at `src/pages/TemplatesPage.tsx:2`: + +```tsx +import { useNavigate, useLocation } from 'react-router-dom' +``` + +Then, inside the component after `const navigate = useNavigate()` +(`src/pages/TemplatesPage.tsx:32`), add: + +```tsx + const location = useLocation() +``` + +- [ ] **Step 2: Auto-open the requested template** + +Add a new effect after the existing "Build live previews" effect (which ends near +`src/pages/TemplatesPage.tsx:55`): + +```tsx + // Deep-link: open a specific template's preview when navigated here with state + // (e.g. from the first-login quickstart guide). + useEffect(() => { + const openId = (location.state as { openTemplateId?: string } | null)?.openTemplateId + if (!openId) return + const match = templates.find((t) => t.id === openId) + if (match) { + setSelected(match) + setDeviceMode('desktop') + } + }, [location.state, templates]) +``` + +- [ ] **Step 3: Verify build** + +Run: `npm run build` +Expected: compiles cleanly. The Restaurant template (`id: 'restaurant-landing'`, +confirmed in `src/lib/templates.ts:1515`) will auto-open its preview overlay when +the quickstart "Open Restaurant template" button routes here. + +- [ ] **Step 4: Commit** + +```bash +git add src/pages/TemplatesPage.tsx +git commit -m "feat: deep-link templates page to auto-open a template preview" +``` + +--- + +## Task 6: Full verification + +**Files:** none (verification only) + +- [ ] **Step 1: Run the full test suite** + +Run: `npm run test` +Expected: all tests pass, including the new `quickstart.test.ts`. + +- [ ] **Step 2: Lint and build** + +Run: `npm run lint && npm run build` +Expected: no errors. + +- [ ] **Step 3: Manual smoke test (requires migration applied)** + +After `supabase db push` against a dev project: +1. Create a fresh account (or set `has_seen_quickstart = false` on your profile + row via SQL). +2. Land on `/dashboard` → the guide appears starting at "Welcome to OpenThorn, + ". +3. Step through with Next/Back; dots track position. +4. Press "Go to Providers" → guide closes, routes to `/providers`. +5. Reload `/dashboard` → guide does NOT reappear (flag persisted). +6. Reset the flag to `false`, reload, advance to the Restaurant slide, press + "Open Restaurant template" → lands on `/templates` with the Restaurant + preview overlay open. + +- [ ] **Step 4: Final commit (if any cleanup)** + +```bash +git add -A +git commit -m "chore: quickstart guide verification cleanup" +``` + +--- + +## Self-Review notes + +- **Spec coverage:** show-once DB flag (Task 1), read/dismiss flow (Task 4), + component + 5 slides with action buttons (Tasks 2–3), Providers/Templates/ + Restaurant deep-link CTAs (Tasks 2 & 5), backfill of existing users (Task 1), + out-of-scope items omitted. All spec sections mapped. +- **Type consistency:** `QuickstartSlide.action` discriminated union is defined + in Task 2 and consumed unchanged in Task 3; `shouldShowQuickstart` signature + matches its use in Task 4; `openTemplateId` state key is identical in the slide + config (Task 2) and the TemplatesPage reader (Task 5). +- **No placeholders:** every code step contains complete, copy-ready content. From e57b11ed8a0da2e25c0711da2fbce6c2d11ff6a3 Mon Sep 17 00:00:00 2001 From: Thomas Tschinkel Date: Sun, 14 Jun 2026 17:04:00 +0200 Subject: [PATCH 3/8] feat(db): add has_seen_quickstart flag to profiles --- supabase/migrations/20260614010000_add_quickstart_flag.sql | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 supabase/migrations/20260614010000_add_quickstart_flag.sql diff --git a/supabase/migrations/20260614010000_add_quickstart_flag.sql b/supabase/migrations/20260614010000_add_quickstart_flag.sql new file mode 100644 index 0000000..e562d93 --- /dev/null +++ b/supabase/migrations/20260614010000_add_quickstart_flag.sql @@ -0,0 +1,7 @@ +-- First-login quickstart guide: track whether a user has seen it. +alter table public.profiles + add column if not exists has_seen_quickstart boolean not null default false; + +-- Existing users have already used the app — don't re-onboard them. +-- New signups created after this migration get the default `false`. +update public.profiles set has_seen_quickstart = true; From 124e03b69742e84c005aaa419c8399d34b819e71 Mon Sep 17 00:00:00 2001 From: Thomas Tschinkel Date: Sun, 14 Jun 2026 17:04:36 +0200 Subject: [PATCH 4/8] feat: add quickstart slide config and show-once predicate --- src/lib/__tests__/quickstart.test.ts | 38 ++++++++++++++++++ src/lib/quickstart.ts | 59 ++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 src/lib/__tests__/quickstart.test.ts create mode 100644 src/lib/quickstart.ts diff --git a/src/lib/__tests__/quickstart.test.ts b/src/lib/__tests__/quickstart.test.ts new file mode 100644 index 0000000..7a1cbfb --- /dev/null +++ b/src/lib/__tests__/quickstart.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect } from 'vitest' +import { QUICKSTART_SLIDES, shouldShowQuickstart } from '../quickstart' + +describe('shouldShowQuickstart', () => { + it('shows only when the flag is explicitly false', () => { + expect(shouldShowQuickstart(false)).toBe(true) + }) + it('does not show when already seen', () => { + expect(shouldShowQuickstart(true)).toBe(false) + }) + it('does not show while unknown/loading (null or undefined)', () => { + expect(shouldShowQuickstart(null)).toBe(false) + expect(shouldShowQuickstart(undefined)).toBe(false) + }) +}) + +describe('QUICKSTART_SLIDES', () => { + it('starts with an advance action and ends with a finish action', () => { + expect(QUICKSTART_SLIDES[0].action.type).toBe('advance') + expect(QUICKSTART_SLIDES[QUICKSTART_SLIDES.length - 1].action.type).toBe('finish') + }) + it('routes the Providers slide to /providers', () => { + const slide = QUICKSTART_SLIDES.find((s) => s.id === 'providers') + expect(slide?.action).toEqual({ type: 'navigate', label: 'Go to Providers', to: '/providers' }) + }) + it('deep-links the Restaurant slide to the restaurant-landing template', () => { + const slide = QUICKSTART_SLIDES.find((s) => s.id === 'restaurant') + expect(slide?.action).toMatchObject({ + type: 'navigate', + to: '/templates', + state: { openTemplateId: 'restaurant-landing' }, + }) + }) + it('has unique slide ids', () => { + const ids = QUICKSTART_SLIDES.map((s) => s.id) + expect(new Set(ids).size).toBe(ids.length) + }) +}) diff --git a/src/lib/quickstart.ts b/src/lib/quickstart.ts new file mode 100644 index 0000000..403c7ae --- /dev/null +++ b/src/lib/quickstart.ts @@ -0,0 +1,59 @@ +/** Action taken when a quickstart slide's primary button is pressed. */ +export type QuickstartAction = + | { type: 'advance'; label: string } + | { type: 'finish'; label: string } + | { type: 'navigate'; label: string; to: string; state?: Record } + +export interface QuickstartSlide { + id: string + heading: string + body: string + action: QuickstartAction +} + +/** + * Slides shown to a brand-new user on their first dashboard visit. + * `navigate` actions close the guide (persisting the flag) and route the user. + */ +export const QUICKSTART_SLIDES: QuickstartSlide[] = [ + { + id: 'welcome', + heading: 'Welcome to OpenThorn', + body: 'OpenThorn builds complete websites from a single prompt — using your own AI provider key.', + action: { type: 'advance', label: 'Next' }, + }, + { + id: 'providers', + heading: 'Connect a provider', + body: 'Your API key stays yours (BYOK). Add it under Providers in the sidebar to start generating.', + action: { type: 'navigate', label: 'Go to Providers', to: '/providers' }, + }, + { + id: 'templates', + heading: 'Browse Templates', + body: 'Prefer a head start? Production-ready starting points live under Templates.', + action: { type: 'navigate', label: 'Open Templates', to: '/templates' }, + }, + { + id: 'restaurant', + heading: 'Try the Restaurant Landing template', + body: 'Open Templates, click a card to preview it, then “Use this template” to customize it with AI.', + action: { + type: 'navigate', + label: 'Open Restaurant template', + to: '/templates', + state: { openTemplateId: 'restaurant-landing' }, + }, + }, + { + id: 'build', + heading: 'Build & deploy', + body: 'Describe your idea in the prompt box on the dashboard, then deploy your site when it’s ready.', + action: { type: 'finish', label: 'Get started' }, + }, +] + +/** Show the guide only when the persisted flag is explicitly false. */ +export function shouldShowQuickstart(hasSeen: boolean | null | undefined): boolean { + return hasSeen === false +} From 643490bf89daa09adb775815d3e055e35055b9be Mon Sep 17 00:00:00 2001 From: Thomas Tschinkel Date: Sun, 14 Jun 2026 17:05:12 +0200 Subject: [PATCH 5/8] feat: add QuickstartGuide modal component --- .../QuickstartGuide.module.css | 120 ++++++++++++++++++ .../QuickstartGuide/QuickstartGuide.tsx | 90 +++++++++++++ 2 files changed, 210 insertions(+) create mode 100644 src/components/QuickstartGuide/QuickstartGuide.module.css create mode 100644 src/components/QuickstartGuide/QuickstartGuide.tsx diff --git a/src/components/QuickstartGuide/QuickstartGuide.module.css b/src/components/QuickstartGuide/QuickstartGuide.module.css new file mode 100644 index 0000000..0db8516 --- /dev/null +++ b/src/components/QuickstartGuide/QuickstartGuide.module.css @@ -0,0 +1,120 @@ +.backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.55); + backdrop-filter: blur(4px); + z-index: 300; + display: grid; + place-items: center; + padding: 1.5rem; + animation: pageFade 0.2s ease both; +} + +.modal { + position: relative; + width: 100%; + max-width: 460px; + background: var(--color-bg); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 20px; + padding: 2.25rem 2rem 1.5rem; + box-shadow: 0 24px 80px rgba(0, 0, 0, 0.5); + text-align: center; + animation: pageRise 0.25s ease both; +} + +.close { + position: absolute; + top: 1rem; + right: 1rem; + width: 32px; + height: 32px; + display: grid; + place-items: center; + border-radius: 8px; + color: var(--color-text); + opacity: 0.6; + transition: opacity 0.15s, background 0.15s; +} +.close:hover { opacity: 1; background: rgba(255, 255, 255, 0.08); } + +.eyebrow { + display: inline-block; + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--color-accent); + margin-bottom: 0.85rem; +} + +.heading { + font-size: 1.5rem; + font-weight: 700; + letter-spacing: -0.02em; + margin-bottom: 0.75rem; + color: var(--color-text); +} + +.body { + font-size: 0.95rem; + line-height: 1.65; + color: var(--color-text); + opacity: 0.75; + margin: 0 auto 1.5rem; + max-width: 36ch; +} + +.dots { + display: flex; + justify-content: center; + gap: 0.4rem; + margin-bottom: 1.5rem; +} +.dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.2); + transition: background 0.2s, transform 0.2s; +} +.dotActive { background: var(--color-accent); transform: scale(1.25); } + +.actions { + display: flex; + gap: 0.75rem; + justify-content: center; +} + +.primary { + flex: 1; + max-width: 280px; + padding: 0.8rem 1.5rem; + background: var(--color-accent); + color: #fff; + border-radius: 12px; + font-weight: 600; + font-size: 0.95rem; + transition: opacity 0.15s, transform 0.15s; +} +.primary:hover { opacity: 0.9; transform: translateY(-1px); } + +.back { + padding: 0.8rem 1.25rem; + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 12px; + color: var(--color-text); + font-weight: 500; + font-size: 0.95rem; + transition: border-color 0.15s; +} +.back:hover { border-color: var(--color-accent); } + +.skip { + margin-top: 1rem; + font-size: 0.8rem; + color: var(--color-text); + opacity: 0.5; + transition: opacity 0.15s; +} +.skip:hover { opacity: 0.85; } diff --git a/src/components/QuickstartGuide/QuickstartGuide.tsx b/src/components/QuickstartGuide/QuickstartGuide.tsx new file mode 100644 index 0000000..587c57c --- /dev/null +++ b/src/components/QuickstartGuide/QuickstartGuide.tsx @@ -0,0 +1,90 @@ +import { useState, useEffect, useCallback } from 'react' +import { useNavigate } from 'react-router-dom' +import { QUICKSTART_SLIDES } from '../../lib/quickstart' +import styles from './QuickstartGuide.module.css' + +interface QuickstartGuideProps { + firstName: string + /** Called whenever the guide is dismissed (finish, navigate, or close). The + * parent persists the has_seen_quickstart flag here. */ + onClose: () => void +} + +export default function QuickstartGuide({ firstName, onClose }: QuickstartGuideProps) { + const navigate = useNavigate() + const [step, setStep] = useState(0) + const slide = QUICKSTART_SLIDES[step] + const isFirst = step === 0 + const total = QUICKSTART_SLIDES.length + + const handleAction = useCallback(() => { + const action = slide.action + if (action.type === 'advance') { + setStep((s) => Math.min(s + 1, total - 1)) + return + } + // Both 'finish' and 'navigate' dismiss the guide. + onClose() + if (action.type === 'navigate') { + navigate(action.to, action.state ? { state: action.state } : undefined) + } + }, [slide, onClose, navigate, total]) + + // Escape closes the guide. + useEffect(() => { + const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose() } + document.addEventListener('keydown', handler) + return () => document.removeEventListener('keydown', handler) + }, [onClose]) + + const heading = slide.id === 'welcome' + ? `Welcome to OpenThorn, ${firstName}` + : slide.heading + + return ( +
{ if (e.target === e.currentTarget) onClose() }} + role="dialog" + aria-modal="true" + aria-label="Quickstart guide" + > +
+ + + Getting started · {step + 1}/{total} +

{heading}

+

{slide.body}

+ + + +
+ {!isFirst && ( + + )} + +
+ + +
+
+ ) +} From a4d917a094ea0a813bcf68899f9b1bd7e35ee8aa Mon Sep 17 00:00:00 2001 From: Thomas Tschinkel Date: Sun, 14 Jun 2026 17:06:47 +0200 Subject: [PATCH 6/8] feat: show quickstart guide to first-login users on dashboard --- src/pages/DashboardPage.tsx | 38 +++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/pages/DashboardPage.tsx b/src/pages/DashboardPage.tsx index a04f7c9..d29eef5 100644 --- a/src/pages/DashboardPage.tsx +++ b/src/pages/DashboardPage.tsx @@ -7,6 +7,8 @@ import { usePageTitle } from '../lib/usePageTitle' import type { AgentThinkingLevel } from '../lib/agent-thinking' import DashboardSidebar, { type ProjectFilter, type SidebarNotification } from '../components/DashboardSidebar/DashboardSidebar' import PromptInput from '../components/PromptInput/PromptInput' +import QuickstartGuide from '../components/QuickstartGuide/QuickstartGuide' +import { shouldShowQuickstart } from '../lib/quickstart' import FloatingParticles from '../components/FloatingParticles/FloatingParticles' import type { SelectedModel } from '../components/ModelSelector/ModelSelector' import styles from './DashboardPage.module.css' @@ -108,6 +110,7 @@ export default function DashboardPage() { return parseStoredJson(localStorage.getItem('dashboard:selectedModel'), null) }) const [sidebarOpen, setSidebarOpen] = useState(false) + const [showQuickstart, setShowQuickstart] = useState(false) const visiblePrompts = showAllPrompts ? examplePrompts : examplePrompts.slice(0, INITIAL_VISIBLE) @@ -285,6 +288,27 @@ export default function DashboardPage() { } }, [user]) + // First-login quickstart guide — show once per account. + useEffect(() => { + if (!user) return + let cancelled = false + supabase + .from('profiles') + .select('has_seen_quickstart') + .eq('id', user.id) + .single() + .then(({ data, error }) => { + if (error) { + logError('DashboardQuickstartFlag', error) + return + } + if (!cancelled && shouldShowQuickstart(data?.has_seen_quickstart)) { + setShowQuickstart(true) + } + }) + return () => { cancelled = true } + }, [user]) + // Fetch and subscribe to global notifications for the dashboard bell. useEffect(() => { if (!user) return @@ -506,6 +530,16 @@ export default function DashboardPage() { setPromptDefault(prompt) } + const handleQuickstartClose = useCallback(async () => { + setShowQuickstart(false) + if (!user) return + const { error } = await supabase + .from('profiles') + .update({ has_seen_quickstart: true }) + .eq('id', user.id) + if (error) logError('DashboardQuickstartDismiss', error) + }, [user]) + const filteredProjects = projects .filter((p) => { if (activeFilter === 'starred') return p.starred @@ -1029,6 +1063,10 @@ export default function DashboardPage() { + {showQuickstart && ( + + )} + {/* Publish to Community modal — outside root to avoid overflow:hidden stacking context */} {publishingProject && (
{ if (e.target === e.currentTarget) setPublishingProject(null) }}> From eeb509e91fb4906f0e4a1548fac1785fef82feab Mon Sep 17 00:00:00 2001 From: Thomas Tschinkel Date: Sun, 14 Jun 2026 17:06:47 +0200 Subject: [PATCH 7/8] feat: deep-link templates page to auto-open a template preview --- src/pages/TemplatesPage.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/pages/TemplatesPage.tsx b/src/pages/TemplatesPage.tsx index 232beaa..25c6288 100644 --- a/src/pages/TemplatesPage.tsx +++ b/src/pages/TemplatesPage.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback } from 'react' -import { useNavigate } from 'react-router-dom' +import { useNavigate, useLocation } from 'react-router-dom' import { useAuth } from '../lib/AuthContext' import { supabase } from '../lib/supabase' import { buildPreview } from '../lib/preview-bundle' @@ -30,6 +30,7 @@ export default function TemplatesPage() { }) const { user, loading } = useAuth() const navigate = useNavigate() + const location = useLocation() const [htmlMap, setHtmlMap] = useState>({}) const [selected, setSelected] = useState