Skip to content

Commit 2b69187

Browse files
feat: add post-registration onboarding checklist (closes #768)
Adds an OnboardingChecklist component rendered on the dashboard that tracks 6 setup steps (verify email, add avatar, write bio, connect a social link, create first milestone, share your page) with a progress bar and per-step links. Dismissible per-account via localStorage, and stays hidden once dismissed.
1 parent 224e04b commit 2b69187

2 files changed

Lines changed: 164 additions & 0 deletions

File tree

frontend/src/app/dashboard/page.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useRouter } from "next/navigation";
55
import Link from "next/link";
66
import { AppShell } from "@/components/app-shell";
77
import { Toast } from "@/components/toast";
8+
import { OnboardingChecklist } from "@/components/onboarding-checklist";
89
import {
910
TrendingUp, Users, Wallet, Activity,
1011
ArrowUpRight, ArrowDownRight, Plus, Edit2, Trash2, X, Link2, Eye, EyeOff, Copy, Check, ChevronDown, ChevronRight, Download
@@ -393,6 +394,10 @@ export default function DashboardPage() {
393394
</div>
394395
</header>
395396

397+
{username && (
398+
<OnboardingChecklist username={username} milestoneCount={milestones.length} />
399+
)}
400+
396401
{/* Summary Cards */}
397402
{stats && (
398403
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
import Link from "next/link";
5+
import { Check } from "lucide-react";
6+
import { apiFetch } from "@/lib/api-client";
7+
8+
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:4000";
9+
const DISMISSED_KEY_PREFIX = "onboardingChecklistDismissed:";
10+
11+
type ChecklistItem = {
12+
key: string;
13+
label: string;
14+
done: boolean;
15+
href: string;
16+
};
17+
18+
type Props = {
19+
username: string;
20+
milestoneCount: number;
21+
};
22+
23+
export function OnboardingChecklist({ username, milestoneCount }: Props) {
24+
const [dismissed, setDismissed] = useState(true); // default hidden until we know state
25+
const [profile, setProfile] = useState<{
26+
emailVerified: boolean;
27+
avatarUrl: string | null;
28+
bio: string;
29+
websiteUrl: string | null;
30+
twitterHandle: string | null;
31+
githubHandle: string | null;
32+
} | null>(null);
33+
34+
useEffect(() => {
35+
const dismissedKey = `${DISMISSED_KEY_PREFIX}${username}`;
36+
if (localStorage.getItem(dismissedKey) === "true") {
37+
return; // stay dismissed, don't bother fetching
38+
}
39+
40+
apiFetch(`${API_BASE_URL}/profiles/${username}`)
41+
.then(async (res) => {
42+
if (!res.ok) return;
43+
const data = await res.json();
44+
setProfile({
45+
emailVerified: Boolean(data.emailVerified),
46+
avatarUrl: data.avatarUrl ?? null,
47+
bio: data.bio ?? "",
48+
websiteUrl: data.websiteUrl ?? null,
49+
twitterHandle: data.twitterHandle ?? null,
50+
githubHandle: data.githubHandle ?? null,
51+
});
52+
setDismissed(false);
53+
})
54+
.catch(() => {});
55+
}, [username]);
56+
57+
if (dismissed || !profile) return null;
58+
59+
const items: ChecklistItem[] = [
60+
{
61+
key: "email",
62+
label: "Verify your email",
63+
done: profile.emailVerified,
64+
href: "/settings",
65+
},
66+
{
67+
key: "avatar",
68+
label: "Add an avatar",
69+
done: !!profile.avatarUrl,
70+
href: `/profile/${username}/edit`,
71+
},
72+
{
73+
key: "bio",
74+
label: "Write a bio",
75+
done: profile.bio.trim().length > 0,
76+
href: `/profile/${username}/edit`,
77+
},
78+
{
79+
key: "social",
80+
label: "Connect a social link",
81+
done: !!(profile.websiteUrl || profile.twitterHandle || profile.githubHandle),
82+
href: `/profile/${username}/edit`,
83+
},
84+
{
85+
key: "milestone",
86+
label: "Create your first milestone",
87+
done: milestoneCount > 0,
88+
href: "/dashboard",
89+
},
90+
{
91+
key: "share",
92+
label: "Share your page",
93+
done: false, // no on-chain/off-chain signal for this yet — always actionable
94+
href: `/profile/${username}`,
95+
},
96+
];
97+
98+
const completedCount = items.filter((i) => i.done).length;
99+
const allComplete = completedCount === items.length;
100+
101+
function handleDismiss() {
102+
localStorage.setItem(`${DISMISSED_KEY_PREFIX}${username}`, "true");
103+
setDismissed(true);
104+
}
105+
106+
return (
107+
<section className="rounded-2xl border border-white/10 bg-white/5 p-6 space-y-4">
108+
<div className="flex items-center justify-between">
109+
<h2 className="text-sm font-semibold uppercase tracking-widest text-white/60">
110+
Getting started
111+
</h2>
112+
<button
113+
onClick={handleDismiss}
114+
className="text-xs text-white/40 hover:text-white"
115+
>
116+
{allComplete ? "Dismiss" : "Got it"}
117+
</button>
118+
</div>
119+
120+
<div>
121+
<div className="flex items-center justify-between text-xs text-white/50 mb-1">
122+
<span>
123+
{completedCount} of {items.length} steps complete
124+
</span>
125+
</div>
126+
<div className="h-1.5 w-full rounded-full bg-white/10 overflow-hidden">
127+
<div
128+
className="h-full rounded-full bg-[#00e5b0] transition-all"
129+
style={{ width: `${(completedCount / items.length) * 100}%` }}
130+
/>
131+
</div>
132+
</div>
133+
134+
<ul className="space-y-2">
135+
{items.map((item) => (
136+
<li key={item.key}>
137+
<Link
138+
href={item.href}
139+
className={`flex items-center gap-2 rounded-lg px-3 py-2 text-sm transition ${
140+
item.done
141+
? "text-white/40"
142+
: "text-white hover:bg-white/[0.04]"
143+
}`}
144+
>
145+
<span
146+
className={`flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full border ${
147+
item.done ? "border-mint bg-mint/20 text-mint" : "border-white/20"
148+
}`}
149+
>
150+
{item.done && <Check size={10} />}
151+
</span>
152+
<span className={item.done ? "line-through" : ""}>{item.label}</span>
153+
</Link>
154+
</li>
155+
))}
156+
</ul>
157+
</section>
158+
);
159+
}

0 commit comments

Comments
 (0)