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.
This commit is contained in:
@@ -96,6 +96,63 @@ export default async function DashboardPage() {
|
|||||||
{ label: 'Unlinked Payments', value: unlinkedPayments ?? 0, Ico: IcoPay, sub: 'needs reconciliation', warn: (unlinkedPayments ?? 0) > 0 },
|
{ 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 (
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
<div className="bg-sidebar px-4 md:px-6 py-3 flex items-center justify-between">
|
||||||
|
<PageTitle>Dashboard</PageTitle>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 md:p-6">
|
||||||
|
<Empty className="bg-card border border-border">
|
||||||
|
<EmptyHeader>
|
||||||
|
<EmptyMedia variant="icon"><IcoPlus /></EmptyMedia>
|
||||||
|
<EmptyTitle>Welcome, {firstName}. Let's set up your space.</EmptyTitle>
|
||||||
|
<EmptyDescription>
|
||||||
|
Your space is empty. These first steps get it running. This panel goes away once there is activity.
|
||||||
|
</EmptyDescription>
|
||||||
|
</EmptyHeader>
|
||||||
|
<div className="grid sm:grid-cols-2 gap-3 w-full max-w-2xl mt-2">
|
||||||
|
{steps.map(({ href, Ico, title, desc }) => (
|
||||||
|
<Link
|
||||||
|
key={href}
|
||||||
|
href={href}
|
||||||
|
className="flex items-start gap-3 text-left bg-background border border-border rounded-lg p-4 hover:border-primary transition"
|
||||||
|
>
|
||||||
|
<span className="w-8 h-8 rounded bg-muted flex items-center justify-center flex-shrink-0">
|
||||||
|
<Ico className="w-4 h-4 text-primary" />
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<span className="block font-sans text-sm font-medium text-foreground">{title}</span>
|
||||||
|
<span className="block font-sans text-xs text-muted-foreground mt-0.5">{desc}</span>
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Empty>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="min-h-screen bg-background">
|
||||||
<div className="bg-sidebar px-4 md:px-6 py-3 flex items-center justify-between">
|
<div className="bg-sidebar px-4 md:px-6 py-3 flex items-center justify-between">
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ export function OnboardingFlow({ spaceName, steps, canSkip, profile: initialProf
|
|||||||
const [busy, setBusy] = useState(false)
|
const [busy, setBusy] = useState(false)
|
||||||
const [acked, setAcked] = useState<Record<string, boolean>>({})
|
const [acked, setAcked] = useState<Record<string, boolean>>({})
|
||||||
const [profile, setProfile] = useState(initialProfile)
|
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 step = steps[index]
|
||||||
const isLast = index === steps.length - 1
|
const isLast = index === steps.length - 1
|
||||||
@@ -167,8 +170,11 @@ export function OnboardingFlow({ spaceName, steps, canSkip, profile: initialProf
|
|||||||
<label className="font-mono text-[10px] tracking-widest text-muted-foreground uppercase block mb-1">Skills (comma separated)</label>
|
<label className="font-mono text-[10px] tracking-widest text-muted-foreground uppercase block mb-1">Skills (comma separated)</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
defaultValue={profile.skills.join(', ')}
|
value={skillsText}
|
||||||
onChange={e => setProfile({ ...profile, skills: e.target.value.split(',').map(s => s.trim()).filter(Boolean).slice(0, 40) })}
|
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"
|
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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -201,6 +207,9 @@ export function OnboardingFlow({ spaceName, steps, canSkip, profile: initialProf
|
|||||||
<span className="font-sans text-sm text-foreground">{ackLabel}</span>
|
<span className="font-sans text-sm text-foreground">{ackLabel}</span>
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
{requireAck && !acked[step.id] && (
|
||||||
|
<p className="font-sans text-xs text-muted-foreground">Check the box above to continue.</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user