From 39e182418d2978383cb510d6627b71a726e59d33 Mon Sep 17 00:00:00 2001 From: bot-hermes Date: Wed, 29 Jul 2026 19:26:23 +0000 Subject: [PATCH] 27: onboarding flow - first-run sheet with 3 steps (domain, task, habit) --- .../components/onboarding/onboarding-flow.tsx | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 apps/web/components/onboarding/onboarding-flow.tsx diff --git a/apps/web/components/onboarding/onboarding-flow.tsx b/apps/web/components/onboarding/onboarding-flow.tsx new file mode 100644 index 0000000..0c60f72 --- /dev/null +++ b/apps/web/components/onboarding/onboarding-flow.tsx @@ -0,0 +1,151 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { + Sheet, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, +} from '@/components/ui/sheet'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { toast } from 'sonner'; +import { Rocket, ListTodo, Flame } from 'lucide-react'; + +const ONBOARDED_KEY = 'pe_onboarded'; + +interface OnboardingFlowProps { + onComplete: () => void; +} + +export function OnboardingFlow({ onComplete }: OnboardingFlowProps) { + const [open, setOpen] = useState(false); + const [step, setStep] = useState(0); + const [domainName, setDomainName] = useState(''); + + useEffect(() => { + if (typeof window === 'undefined') return; + const onboarded = localStorage.getItem(ONBOARDED_KEY); + if (!onboarded) { + setOpen(true); + } + }, []); + + function handleDismiss() { + localStorage.setItem(ONBOARDED_KEY, 'true'); + setOpen(false); + onComplete(); + } + + async function handleCreateDomain() { + if (!domainName.trim()) { + toast.error('Please enter a domain name'); + return; + } + try { + const res = await fetch('/api/domains', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: domainName.trim() }), + }); + if (!res.ok) throw new Error('Failed to create domain'); + toast.success('Domain created!'); + setStep(1); + } catch { + toast.error('Failed to create domain'); + } + } + + const steps = [ + { + title: 'Pick your primary domain', + description: 'A domain is your workspace — a container for tasks, habits, and projects.', + icon: , + content: ( +
+ + setDomainName(e.target.value)} + placeholder="e.g. Personal, Work, Side Project" + autoFocus + /> + +
+ ), + }, + { + title: 'Create your first task', + description: 'Tasks are the building blocks of your workflow. Create one to get started.', + icon: , + content: ( +
+

+ Use the + button in the top bar or press{' '} + C on the Tasks page to create a new task. +

+ +
+ ), + }, + { + title: 'Add a habit', + description: 'Build streaks and track progress on things you do regularly.', + icon: , + content: ( +
+

+ Head to the Habits page and click New habit to start tracking something daily or weekly. +

+ +
+ ), + }, + ]; + + const current = steps[step]; + + return ( + { if (!v) handleDismiss(); }}> + + +
+ {current.icon} +
+ {current.title} + {current.description} +
+
+
+ + {current.content} + + +
+ {steps.map((_, i) => ( +
+ ))} +
+ + + + + ); +}