From a1ea92a71c22511d9580bc8fa19e5a225264b572 Mon Sep 17 00:00:00 2001 From: Moheeb Zara Date: Sat, 16 May 2026 14:10:29 -0700 Subject: [PATCH] feat(ux): founder first-run dashboard + onboarding skills/ack fixes Tier 2. Admin/board alone in an empty space now gets a guided getting-started checklist (invite members, set up onboarding, first task, connect payments) instead of a grid of zeros; reverts to the normal dashboard once there is any activity. Onboarding: Skills is now a controlled field (edits survive Back/forward, no normalize surprise) and a hint appears when a required acknowledgement is unchecked. --- app/(app)/dashboard/page.tsx | 57 ++++++++++++++++++++++++++++++ app/onboarding/onboarding-flow.tsx | 13 +++++-- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/app/(app)/dashboard/page.tsx b/app/(app)/dashboard/page.tsx index 601f34b..b072e01 100644 --- a/app/(app)/dashboard/page.tsx +++ b/app/(app)/dashboard/page.tsx @@ -96,6 +96,63 @@ export default async function DashboardPage() { { label: 'Unlinked Payments', value: unlinkedPayments ?? 0, Ico: IcoPay, sub: 'needs reconciliation', warn: (unlinkedPayments ?? 0) > 0 }, ] + // First-run: an admin/board member alone in a space with nothing set up yet + // gets a guided checklist instead of a grid of zeros. + const isFirstRun = + isAdminOrBoard && + (activeMembers ?? 0) <= 1 && + (openTasks ?? 0) === 0 && + (unlinkedPayments ?? 0) === 0 && + (projects ?? []).length === 0 && + (activity ?? []).length === 0 && + pendingProposals.length === 0 && + pendingIncidents.length === 0 + + if (isFirstRun) { + const steps = [ + { href: '/members', Ico: IcoUsers, title: 'Invite or add members', desc: 'Add people directly, or share an invite code from Customize.' }, + { href: '/customize', Ico: IcoPlus, title: 'Set up onboarding', desc: 'Decide what new members see and agree to when they join.' }, + { href: '/tasks', Ico: IcoTask, title: 'Create your first task or chore', desc: 'Capture the recurring upkeep your space depends on.' }, + { href: '/settings', Ico: IcoPay, title: 'Connect payments', desc: 'Link a payment platform to track dues and reconcile income.' }, + ] + const firstName = member.display_name?.split(' ')[0] || 'there' + return ( +
+
+ Dashboard +
+
+ + + + Welcome, {firstName}. Let's set up your space. + + Your space is empty. These first steps get it running. This panel goes away once there is activity. + + +
+ {steps.map(({ href, Ico, title, desc }) => ( + + + + + + {title} + {desc} + + + ))} +
+
+
+
+ ) + } + return (
diff --git a/app/onboarding/onboarding-flow.tsx b/app/onboarding/onboarding-flow.tsx index 04ea0e5..bb5e581 100644 --- a/app/onboarding/onboarding-flow.tsx +++ b/app/onboarding/onboarding-flow.tsx @@ -42,6 +42,9 @@ export function OnboardingFlow({ spaceName, steps, canSkip, profile: initialProf const [busy, setBusy] = useState(false) const [acked, setAcked] = useState>({}) const [profile, setProfile] = useState(initialProfile) + // Controlled raw text for the skills field so edits survive navigating + // Back/forward and the user sees exactly what they typed (no normalize). + const [skillsText, setSkillsText] = useState(initialProfile.skills.join(', ')) const step = steps[index] const isLast = index === steps.length - 1 @@ -167,8 +170,11 @@ export function OnboardingFlow({ spaceName, steps, canSkip, profile: initialProf setProfile({ ...profile, skills: e.target.value.split(',').map(s => s.trim()).filter(Boolean).slice(0, 40) })} + value={skillsText} + onChange={e => { + setSkillsText(e.target.value) + setProfile({ ...profile, skills: e.target.value.split(',').map(s => s.trim()).filter(Boolean).slice(0, 40) }) + }} className="w-full bg-background border border-border text-foreground font-sans text-sm rounded px-3 py-2 focus:outline-none focus:border-primary" />
@@ -201,6 +207,9 @@ export function OnboardingFlow({ spaceName, steps, canSkip, profile: initialProf {ackLabel} )} + {requireAck && !acked[step.id] && ( +

Check the box above to continue.

+ )}
)}