22
33import { useSyncExternalStore } from "react" ;
44import Link from "next/link" ;
5+ import { api } from "@/server/trpc/react" ;
6+ import { useShellActions } from "@/components/Create/ShellActionsProvider" ;
57
68const KEY = "codu.onboarding.dismissed" ;
79
8- const STEPS = [
9- { label : "Pick your topics" , href : "/feed" } ,
10- { label : "Follow 3 builders" , href : "/discussions" } ,
11- { label : "Post your first tip" , href : "/create?kind=til" } ,
12- ] ;
13-
1410// Tiny external store so dismissal is read without setState-in-effect and stays
1511// SSR-safe (server snapshot = not dismissed → banner renders, client reads
1612// localStorage on hydration).
@@ -23,18 +19,43 @@ const isDismissed = () =>
2319 typeof window !== "undefined" && localStorage . getItem ( KEY ) === "1" ;
2420
2521/**
26- * First-run guidance: a dismissible "first win in 3 steps" banner. Onboarding is
27- * the #1 retention lever, so we guide the first action. Mirrors
28- * ui_kits/app/Feed.jsx → OnboardingBanner.
22+ * First-run guidance: a dismissible "first win in 3 steps" banner. Steps reflect
23+ * REAL completion (topics picked / 3 follows / first post) via
24+ * engagement.onboardingWins — done steps show a mint check + strikethrough. The
25+ * banner hides itself once all three are done. Mirrors ui_kits/app/Feed.jsx.
2926 */
3027export function OnboardingBanner ( ) {
31- const dismissed = useSyncExternalStore (
32- subscribe ,
33- isDismissed ,
34- ( ) => false ,
35- ) ;
28+ const dismissed = useSyncExternalStore ( subscribe , isDismissed , ( ) => false ) ;
29+ const { openTopics, openCompose } = useShellActions ( ) ;
30+ const { data : wins } = api . engagement . onboardingWins . useQuery ( ) ;
31+
32+ // A step is { label, done, action }. Actions reuse the shell modals so they
33+ // work even when the rail is hidden on mobile.
34+ const steps = [
35+ {
36+ label : "Pick your topics" ,
37+ done : ! ! wins ?. pickedTopics ,
38+ onClick : openTopics ,
39+ href : undefined as string | undefined ,
40+ } ,
41+ {
42+ label : "Follow 3 builders" ,
43+ done : ! ! wins ?. followedThree ,
44+ onClick : undefined ,
45+ href : "/discussions" ,
46+ } ,
47+ {
48+ label : "Post your first tip" ,
49+ done : ! ! wins ?. posted ,
50+ onClick : ( ) => openCompose ( "discussion" ) ,
51+ href : undefined as string | undefined ,
52+ } ,
53+ ] ;
3654
37- if ( dismissed ) return null ;
55+ const allDone = wins ? steps . every ( ( s ) => s . done ) : false ;
56+ if ( dismissed || allDone ) return null ;
57+
58+ const doneCount = steps . filter ( ( s ) => s . done ) . length ;
3859
3960 const dismiss = ( ) => {
4061 localStorage . setItem ( KEY , "1" ) ;
@@ -69,20 +90,63 @@ export function OnboardingBanner() {
6990 ✕
7091 </ button >
7192 </ div >
72- < div className = "mt-4 grid gap-3 sm:grid-cols-2" >
73- { STEPS . map ( ( s , i ) => (
74- < Link
75- key = { s . label }
76- href = { s . href }
77- className = "flex items-center gap-3 rounded-md border border-hairline bg-elevated p-3 transition-colors hover:border-strong"
78- >
79- < span className = "flex h-[22px] w-[22px] shrink-0 items-center justify-center rounded-full border border-strong font-mono text-xs text-faint" >
80- { i + 1 }
81- </ span >
82- < span className = "text-sm font-medium text-fg" > { s . label } </ span >
83- </ Link >
93+
94+ { /* progress rail */ }
95+ < div className = "mt-4 flex gap-1.5" >
96+ { steps . map ( ( s , i ) => (
97+ < span
98+ key = { i }
99+ className = { `h-1 flex-1 rounded-full ${
100+ s . done ? "bg-accent" : "bg-elevated"
101+ } `}
102+ />
84103 ) ) }
85104 </ div >
105+
106+ < div className = "mt-4 grid gap-3 sm:grid-cols-2" >
107+ { steps . map ( ( s ) => {
108+ const inner = (
109+ < >
110+ < span
111+ className = { `flex h-[22px] w-[22px] shrink-0 items-center justify-center rounded-full text-xs ${
112+ s . done
113+ ? "bg-accent font-bold text-on-accent"
114+ : "border border-strong text-faint"
115+ } `}
116+ >
117+ { s . done ? "✓" : "" }
118+ </ span >
119+ < span
120+ className = { `text-sm font-medium ${
121+ s . done ? "text-muted line-through" : "text-fg"
122+ } `}
123+ >
124+ { s . label }
125+ </ span >
126+ </ >
127+ ) ;
128+ const className = `flex items-center gap-3 rounded-md border border-hairline p-3 text-left transition-colors ${
129+ s . done ? "bg-transparent" : "bg-elevated hover:border-strong"
130+ } `;
131+ return s . href ? (
132+ < Link key = { s . label } href = { s . href } className = { className } >
133+ { inner }
134+ </ Link >
135+ ) : (
136+ < button
137+ key = { s . label }
138+ type = "button"
139+ onClick = { s . onClick }
140+ className = { className }
141+ >
142+ { inner }
143+ </ button >
144+ ) ;
145+ } ) }
146+ </ div >
147+ < p className = "mt-3 font-mono text-xs text-faint" >
148+ { doneCount } /3 done
149+ </ p >
86150 </ div >
87151 </ div >
88152 ) ;
0 commit comments