# Phase 30: Onboarding Redesign — Research **Researched:** 2026-04-12 **Status:** Complete ## Executive Summary Phase 30 replaces the current 4-step modal onboarding wizard with a full-screen, catalog-driven, hobby-personalized experience. The existing codebase has strong infrastructure for catalog items (globalItems + tags + globalItemTags), discovery queries, and item linking — the main work is building new frontend components and one new backend endpoint for popular items by tag/hobby. ## Current State Analysis ### Existing Onboarding (`OnboardingWizard.tsx`) - 4-step modal: Welcome → Create Category → Add Item → Done - Centered card overlay (`fixed inset-0 z-50`, `max-w-md`, backdrop blur) - Manual entry — user types category name, item name, weight, price - Skip available on all steps - `onboardingComplete` setting tracked in `settings` table (key-value, per-user) - Trigger logic in `__root.tsx` (~lines 97-107): shows wizard when authenticated + `onboardingComplete !== "true"` + not dismissed ### Catalog Infrastructure (Reusable) - **globalItems table**: brand, model, category, weightGrams, priceCents, imageUrl, description, etc. - **tags table**: id, name (unique) - **globalItemTags table**: many-to-many join (globalItemId, tagId) - **searchGlobalItems()**: ILIKE search with AND-logic tag filtering — exactly what hobby filtering needs - **getGlobalItemWithOwnerCount()**: single item + count of users who linked it — provides "popularity" metric - **GlobalItemCard component**: displays brand, model, image, weight, price, category badges - **useGlobalItems hook**: fetches with query + tag params - **Discovery service**: `getRecentGlobalItems()`, `getPopularSetups()`, `getTrendingCategories()` — patterns for a new `getPopularItemsByTags()` query ### Item Linking Flow - `useLinkItem` mutation: `POST /api/items/:itemId/link` with `{ globalItemId }` - This creates a user item linked to a global catalog item - For onboarding batch-add, we need a new batch endpoint or loop through individual creates ## Technical Approach ### Backend: New Endpoint — Popular Items by Hobby Tags **Needed:** `GET /api/discovery/popular-items?tags=bikepacking,hiking&limit=20` Returns global items filtered by tags, ordered by owner count (number of user items referencing each global item). Pattern follows existing `getPopularSetups()` with owner count from `items.globalItemId`. ```sql SELECT gi.*, COUNT(i.id) as owner_count FROM global_items gi LEFT JOIN items i ON i.global_item_id = gi.id JOIN global_item_tags git ON git.global_item_id = gi.id JOIN tags t ON t.id = git.tag_id WHERE t.name IN (...hobby_tags) GROUP BY gi.id ORDER BY owner_count DESC, gi.id DESC LIMIT ? ``` ### Backend: Batch Add from Catalog **Needed:** `POST /api/onboarding/complete` — batch-creates user items from selected global item IDs, auto-creates categories, marks onboarding complete. Accepts: `{ globalItemIds: number[], hobbyTags: string[] }` - For each selected globalItem: create a user item with `globalItemId` link, using the global item's category to auto-create user categories - Set `onboardingComplete` setting to "true" - Return created items summary This is a single transactional endpoint to avoid partial state. ### Backend: Hobby Tag Mapping Need a predefined mapping of hobby → tags. This can be a static config (no DB table needed): ```ts const HOBBY_TAG_MAP: Record = { bikepacking: ["bikepacking", "cycling", "camping"], hiking: ["hiking", "backpacking", "camping"], climbing: ["climbing", "mountaineering"], cycling: ["cycling", "road-cycling", "gravel"], // extensible... }; ``` Store in a shared constants file. Frontend uses it for hobby card rendering; backend uses it for tag queries. ### Frontend: Full-Screen Onboarding Flow **Component structure:** - `OnboardingFlow.tsx` — top-level full-screen component with step management - `OnboardingWelcome.tsx` — welcome/hero step - `OnboardingHobbyPicker.tsx` — card-based hobby selection - `OnboardingItemBrowser.tsx` — grid of popular items with check/uncheck - `OnboardingReview.tsx` — summary of selections before commit **Routing decision:** Use a single component with internal step state (not TanStack Router routes). Reasons: 1. Onboarding is a temporary, one-time flow — no URL navigation needed 2. Step state is ephemeral — lost on completion 3. Simpler to manage as a controlled component rendered from `__root.tsx` **Reusable components:** - `GlobalItemCard` — adapt for selectable mode (add checkbox overlay) - `LucideIcon` — for hobby card icons - `useFormatters` — weight/price display ### Frontend: Transition Design Full-screen steps with CSS transitions. Each step is a full-viewport div that slides/fades: - Use `framer-motion` or CSS `transition` + `transform` for step transitions - Check if project already has framer-motion — if not, CSS transitions are sufficient - Step indicator: full-width progress bar (not dots) ### Category Auto-Creation Logic When user confirms selections: 1. Group selected global items by their `category` field 2. For each unique category name: check if user already has a category with that name, create if not 3. Create user items in each category, linked to their globalItemId This avoids a manual "create category" step entirely. ## Validation Architecture ### Critical Paths 1. **Hobby selection → tag filtering → item display**: Hobby cards must map to valid tags that return items 2. **Batch selection → review → commit**: Selected items must persist through steps and batch-create atomically 3. **Onboarding trigger**: Must show for new users, must not show after completion 4. **Empty catalog state**: Hobby with no tagged items should show graceful empty state ### Edge Cases - User with no catalog items for their hobby (empty tags) - User selects items, goes back, changes hobby — selections should reset - Browser refresh mid-onboarding — starts over (acceptable since onboarding is quick) - Multiple hobbies selected — combined tag results, deduplicated - Global item has no category — needs fallback category assignment ### Testable Assertions - `GET /api/discovery/popular-items?tags=bikepacking` returns items sorted by owner_count DESC - `POST /api/onboarding/complete` with valid globalItemIds creates items and sets onboardingComplete - OnboardingFlow renders when `onboardingComplete !== "true"` and user is authenticated - Hobby cards render with correct icons and labels - Item selection state persists across steps (hobby → browse → review) - Skipping browse step marks onboarding complete without creating items ## Dependencies - **Phase 28** (Depends on): Must be complete — provides the catalog data foundation - **Existing tags in DB**: The hobby-tag mapping assumes tags like "bikepacking", "hiking" exist in the tags table. If catalog data is sparse, the onboarding will show empty grids. This is acceptable for launch — catalog enrichment (Phase 25) populates tags. ## Risks and Mitigations | Risk | Impact | Mitigation | |------|--------|------------| | Few catalog items tagged for hobbies | Empty onboarding grid | Show "Skip" option prominently; fall back to recent items if tag results < threshold | | Batch item creation fails mid-transaction | Partial state | Wrap in DB transaction — all-or-nothing | | framer-motion dependency bloat | Bundle size | Use CSS transitions instead — no new dependency | | Hobby-tag mapping becomes stale | Irrelevant results | Store mapping in editable config; admin can update | ## RESEARCH COMPLETE