1- import { createSignal , Show } from "solid-js" ;
1+ import {
2+ createSignal ,
3+ createMemo ,
4+ onMount ,
5+ Show ,
6+ Switch ,
7+ Match ,
8+ } from "solid-js" ;
29import { config , updateConfig , CONFIG_STORAGE_KEY } from "../../stores/config" ;
3- import { RepoRef } from "../../services/api" ;
4- import OrgSelector from "./OrgSelector " ;
10+ import { fetchOrgs , type OrgEntry , type RepoRef } from "../../services/api" ;
11+ import { getClient } from "../../services/github " ;
512import RepoSelector from "./RepoSelector" ;
6-
7- const STEPS = [ "Select Organizations" , "Select Repositories" ] as const ;
13+ import LoadingSpinner from "../shared/LoadingSpinner" ;
814
915export default function OnboardingWizard ( ) {
10- const [ step , setStep ] = createSignal ( 0 ) ;
11- const [ selectedOrgs , setSelectedOrgs ] = createSignal < string [ ] > (
12- config . selectedOrgs . length > 0 ? [ ...config . selectedOrgs ] : [ ]
13- ) ;
1416 const [ selectedRepos , setSelectedRepos ] = createSignal < RepoRef [ ] > (
1517 config . selectedRepos . length > 0 ? [ ...config . selectedRepos ] : [ ]
1618 ) ;
1719
18- function handleNext ( ) {
19- if ( step ( ) === 0 ) {
20- updateConfig ( { selectedOrgs : selectedOrgs ( ) } ) ;
21- setStep ( 1 ) ;
20+ const [ loading , setLoading ] = createSignal ( true ) ;
21+ const [ error , setError ] = createSignal < string | null > ( null ) ;
22+ const [ orgEntries , setOrgEntries ] = createSignal < OrgEntry [ ] > ( [ ] ) ;
23+
24+ const allOrgLogins = createMemo ( ( ) => orgEntries ( ) . map ( ( o ) => o . login ) ) ;
25+
26+ async function loadOrgs ( ) {
27+ setLoading ( true ) ;
28+ setError ( null ) ;
29+ try {
30+ const client = getClient ( ) ;
31+ if ( ! client ) throw new Error ( "No GitHub client available" ) ;
32+ const result = await fetchOrgs ( client ) ;
33+ setOrgEntries ( result ) ;
34+ } catch ( err ) {
35+ setError (
36+ err instanceof Error ? err . message : "Failed to load organizations"
37+ ) ;
38+ } finally {
39+ setLoading ( false ) ;
2240 }
2341 }
2442
43+ onMount ( ( ) => {
44+ if ( config . onboardingComplete ) {
45+ window . location . replace ( "/dashboard" ) ;
46+ return ;
47+ }
48+ void loadOrgs ( ) ;
49+ } ) ;
50+
2551 function handleFinish ( ) {
52+ const uniqueOrgs = [ ...new Set ( selectedRepos ( ) . map ( ( r ) => r . owner ) ) ] ;
2653 updateConfig ( {
54+ selectedOrgs : uniqueOrgs ,
2755 selectedRepos : selectedRepos ( ) ,
2856 onboardingComplete : true ,
2957 } ) ;
@@ -32,15 +60,6 @@ export default function OnboardingWizard() {
3260 window . location . replace ( "/dashboard" ) ;
3361 }
3462
35- function handleBack ( ) {
36- setStep ( ( s ) => Math . max ( 0 , s - 1 ) ) ;
37- }
38-
39- const canProceed = ( ) => {
40- if ( step ( ) === 0 ) return selectedOrgs ( ) . length > 0 ;
41- return selectedRepos ( ) . length > 0 ;
42- } ;
43-
4463 return (
4564 < div class = "min-h-screen bg-gray-50 dark:bg-gray-900" >
4665 < div class = "mx-auto max-w-2xl px-4 py-12" >
@@ -50,136 +69,66 @@ export default function OnboardingWizard() {
5069 GitHub Tracker Setup
5170 </ h1 >
5271 < p class = "mt-1 text-sm text-gray-500 dark:text-gray-400" >
53- Step { step ( ) + 1 } of { STEPS . length }
72+ Select the repositories you want to track.
5473 </ p >
5574 </ div >
5675
57- { /* Step indicator */ }
58- < nav class = "mb-8" aria-label = "Progress" >
59- < ol class = "flex items-center justify-center gap-4" >
60- { STEPS . map ( ( label , i ) => (
61- < li class = "flex items-center gap-2" >
62- < span
63- class = { `flex h-7 w-7 items-center justify-center rounded-full text-xs font-semibold ${
64- i < step ( )
65- ? "bg-blue-600 text-white dark:bg-blue-500"
66- : i === step ( )
67- ? "bg-blue-600 text-white ring-4 ring-blue-100 dark:bg-blue-500 dark:ring-blue-900"
68- : "bg-gray-200 text-gray-500 dark:bg-gray-700 dark:text-gray-400"
69- } `}
70- aria-current = { i === step ( ) ? "step" : undefined }
71- >
72- { i < step ( ) ? (
73- < svg
74- class = "h-4 w-4"
75- viewBox = "0 0 20 20"
76- fill = "currentColor"
77- aria-hidden = "true"
78- >
79- < path
80- fill-rule = "evenodd"
81- d = "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
82- clip-rule = "evenodd"
83- />
84- </ svg >
85- ) : (
86- i + 1
87- ) }
88- </ span >
89- < span
90- class = { `text-sm font-medium ${
91- i === step ( )
92- ? "text-gray-900 dark:text-gray-100"
93- : "text-gray-400 dark:text-gray-500"
94- } `}
95- >
96- { label }
97- </ span >
98- { i < STEPS . length - 1 && (
99- < span class = "mx-2 text-gray-300 dark:text-gray-600" >
100- ›
101- </ span >
102- ) }
103- </ li >
104- ) ) }
105- </ ol >
106- </ nav >
107-
108- { /* Step content */ }
76+ { /* Content */ }
10977 < div class = "rounded-xl border border-gray-200 bg-white p-6 shadow-sm dark:border-gray-700 dark:bg-gray-800" >
110- < Show when = { step ( ) === 0 } >
111- < div class = "mb-5" >
112- < h2 class = "text-lg font-semibold text-gray-900 dark:text-gray-100" >
113- Select Organizations
114- </ h2 >
115- < p class = "mt-1 text-sm text-gray-500 dark:text-gray-400" >
116- Choose the GitHub organizations and personal account to track.
117- </ p >
118- </ div >
119- < OrgSelector
120- selected = { selectedOrgs ( ) }
121- onChange = { setSelectedOrgs }
122- />
123- </ Show >
124-
125- < Show when = { step ( ) === 1 } >
126- < div class = "mb-5" >
127- < h2 class = "text-lg font-semibold text-gray-900 dark:text-gray-100" >
128- Select Repositories
129- </ h2 >
130- < p class = "mt-1 text-sm text-gray-500 dark:text-gray-400" >
131- Choose which repositories to track within your selected
132- organizations.
133- </ p >
134- </ div >
135- < RepoSelector
136- selectedOrgs = { selectedOrgs ( ) }
137- selected = { selectedRepos ( ) }
138- onChange = { setSelectedRepos }
139- />
140- </ Show >
78+ < Switch >
79+ < Match when = { error ( ) } >
80+ < div class = "flex flex-col items-center gap-3 py-12" >
81+ < p class = "text-sm text-red-600 dark:text-red-400" >
82+ { error ( ) }
83+ </ p >
84+ < button
85+ type = "button"
86+ onClick = { ( ) => void loadOrgs ( ) }
87+ class = "rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
88+ >
89+ Retry
90+ </ button >
91+ </ div >
92+ </ Match >
93+ < Match when = { loading ( ) } >
94+ < div class = "flex items-center justify-center py-12" >
95+ < LoadingSpinner size = "md" label = "Loading organizations..." />
96+ </ div >
97+ </ Match >
98+ < Match when = { ! loading ( ) && ! error ( ) } >
99+ < div class = "mb-5" >
100+ < h2 class = "text-lg font-semibold text-gray-900 dark:text-gray-100" >
101+ Select Repositories
102+ </ h2 >
103+ < p class = "mt-1 text-sm text-gray-500 dark:text-gray-400" >
104+ Choose which repositories to track.
105+ </ p >
106+ </ div >
107+ < RepoSelector
108+ selectedOrgs = { allOrgLogins ( ) }
109+ orgEntries = { orgEntries ( ) }
110+ selected = { selectedRepos ( ) }
111+ onChange = { setSelectedRepos }
112+ />
113+ </ Match >
114+ </ Switch >
141115 </ div >
142116
143- { /* Navigation buttons */ }
144- < div class = "mt-6 flex items-center justify-between" >
145- < Show
146- when = { step ( ) > 0 }
147- fallback = { < div /> }
148- >
149- < button
150- type = "button"
151- onClick = { handleBack }
152- class = "rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
153- >
154- Back
155- </ button >
156- </ Show >
157-
158- < Show
159- when = { step ( ) === STEPS . length - 1 }
160- fallback = {
161- < button
162- type = "button"
163- onClick = { handleNext }
164- disabled = { ! canProceed ( ) }
165- class = "ml-auto rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-40 dark:bg-blue-500 dark:hover:bg-blue-600"
166- >
167- Next
168- </ button >
169- }
170- >
117+ { /* Navigation buttons — hidden during loading/error to avoid confusion */ }
118+ < Show when = { ! loading ( ) && ! error ( ) } >
119+ < div class = "mt-6 flex items-center justify-end" >
171120 < button
172121 type = "button"
173122 onClick = { handleFinish }
174123 disabled = { selectedRepos ( ) . length === 0 }
175- class = "ml-auto rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-40 dark:bg-blue-500 dark:hover:bg-blue-600"
124+ class = "rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-40 dark:bg-blue-500 dark:hover:bg-blue-600"
176125 >
177126 { selectedRepos ( ) . length === 0
178127 ? "Finish Setup"
179128 : `Finish Setup (${ selectedRepos ( ) . length } ${ selectedRepos ( ) . length === 1 ? "repo" : "repos" } )` }
180129 </ button >
181- </ Show >
182- </ div >
130+ </ div >
131+ </ Show >
183132 </ div >
184133 </ div >
185134 ) ;
0 commit comments