1+ import { createBrowserRouter , RouterProvider , Navigate } from "react-router-dom" ;
12import { useState , useCallback , useEffect } from "react" ;
2- import { Routes , Route , Navigate , useNavigate , useParams } from "react-router-dom" ;
33import { api } from "./hooks/useApi" ;
44import { useAuth } from "./hooks/useAuth" ;
55import { useWebSocket } from "./hooks/useWebSocket" ;
@@ -14,10 +14,19 @@ import { WorkflowsPage } from "./pages/WorkflowsPage";
1414import { BillingPage } from "./pages/BillingPage" ;
1515import { IntegrationsPage } from "./pages/IntegrationsPage" ;
1616import { SettingsPage } from "./pages/SettingsPage" ;
17+ import { LandingPage } from "./pages/LandingPage" ;
18+
19+ function WorkspaceLayout ( ) {
20+ const { loggedIn, logout } = useAuth ( ) ;
21+ // Extract workspaceId and channelId from URL manually since we're in a splat route
22+ const path = window . location . pathname ;
23+ const parts = path . split ( "/" ) . filter ( Boolean ) ;
24+ // URL pattern: /ws/:workspaceId/ch/:channelId
25+ const workspaceId = parts [ 1 ] || undefined ;
26+ const channelId = parts [ 2 ] === "ch" ? parts [ 3 ] : undefined ;
27+
28+ const navigate = ( url : string ) => { window . location . href = url ; } ;
1729
18- function WorkspaceLayout ( { loggedIn, onLogout } : { loggedIn : boolean ; onLogout : ( ) => void } ) {
19- const { workspaceId, channelId } = useParams < { workspaceId : string ; channelId ?: string } > ( ) ;
20- const navigate = useNavigate ( ) ;
2130 const [ workspaces , setWorkspaces ] = useState < Workspace [ ] > ( [ ] ) ;
2231 const [ workspace , setWorkspace ] = useState < Workspace | null > ( null ) ;
2332 const [ channels , setChannels ] = useState < Channel [ ] > ( [ ] ) ;
@@ -29,16 +38,12 @@ function WorkspaceLayout({ loggedIn, onLogout }: { loggedIn: boolean; onLogout:
2938 switch ( msg . type ) {
3039 case "message.new" : {
3140 const m = msg . data as Message ;
32- if ( m . channel_id === channel ?. id ) {
33- setMessages ( ( prev ) => [ ...prev , m ] ) ;
34- }
41+ if ( m . channel_id === channel ?. id ) setMessages ( ( prev ) => [ ...prev , m ] ) ;
3542 break ;
3643 }
3744 case "message.edit" : {
3845 const m = msg . data as Message ;
39- if ( m . channel_id === channel ?. id ) {
40- setMessages ( ( prev ) => prev . map ( ( x ) => ( x . id === m . id ? { ...x , ...m } : x ) ) ) ;
41- }
46+ if ( m . channel_id === channel ?. id ) setMessages ( ( prev ) => prev . map ( ( x ) => ( x . id === m . id ? { ...x , ...m } : x ) ) ) ;
4247 break ;
4348 }
4449 case "message.delete" : {
@@ -51,14 +56,10 @@ function WorkspaceLayout({ loggedIn, onLogout }: { loggedIn: boolean; onLogout:
5156
5257 useWebSocket ( handleWSEvent ) ;
5358
54- // Load workspaces
5559 useEffect ( ( ) => {
56- if ( loggedIn ) {
57- api < Workspace [ ] > ( "/v1/workspaces" ) . then ( setWorkspaces ) . catch ( ( e ) => console . error ( "Failed to load workspaces:" , e ) ) ;
58- }
60+ if ( loggedIn ) api < Workspace [ ] > ( "/v1/workspaces" ) . then ( setWorkspaces ) . catch ( ( ) => { } ) ;
5961 } , [ loggedIn ] ) ;
6062
61- // Select workspace from URL
6263 useEffect ( ( ) => {
6364 if ( ! workspaceId || workspaces . length === 0 ) return ;
6465 const ws = workspaces . find ( ( w ) => w . id === workspaceId ) ;
@@ -67,161 +68,123 @@ function WorkspaceLayout({ loggedIn, onLogout }: { loggedIn: boolean; onLogout:
6768 setChannel ( null ) ;
6869 setMessages ( [ ] ) ;
6970 setActiveThread ( null ) ;
70- api < Channel [ ] > ( `/v1/workspaces/${ ws . id } /channels` ) . then ( setChannels ) . catch ( ( e ) => console . error ( "Failed to load channels:" , e ) ) ;
71+ api < Channel [ ] > ( `/v1/workspaces/${ ws . id } /channels` ) . then ( setChannels ) . catch ( ( ) => { } ) ;
7172 }
7273 } , [ workspaceId , workspaces ] ) ;
7374
74- // Select channel from URL
7575 useEffect ( ( ) => {
7676 if ( ! channelId || channels . length === 0 ) return ;
7777 const ch = channels . find ( ( c ) => c . id === channelId ) ;
7878 if ( ch && ch . id !== channel ?. id ) {
7979 setChannel ( ch ) ;
8080 setActiveThread ( null ) ;
81- api < Message [ ] > ( `/v1/channels/${ ch . id } /messages?limit=50` ) . then ( ( m ) => setMessages ( m || [ ] ) ) . catch ( ( e ) => console . error ( "Failed to load messages:" , e ) ) ;
81+ api < Message [ ] > ( `/v1/channels/${ ch . id } /messages?limit=50` ) . then ( ( m ) => setMessages ( m || [ ] ) ) . catch ( ( ) => { } ) ;
8282 }
8383 } , [ channelId , channels ] ) ;
8484
85- const handleSelectWorkspace = ( ws : Workspace ) => {
86- navigate ( `/ws/${ ws . id } ` ) ;
87- } ;
88-
89- const handleSelectChannel = ( ch : Channel ) => {
90- navigate ( `/ws/${ workspaceId } /ch/${ ch . id } ` ) ;
91- } ;
92-
93- const handleSendMessage = async ( content : string , contentType ?: string ) => {
94- if ( ! channel ) return ;
95- await api ( `/v1/channels/${ channel . id } /messages` , {
96- method : "POST" ,
97- body : JSON . stringify ( { content, content_type : contentType || "text" } ) ,
98- } ) ;
99- } ;
100-
101- const handleEditMessage = async ( msg : Message ) => {
102- const newContent = prompt ( "Edit message:" , msg . content ) ;
103- if ( newContent === null || newContent === msg . content ) return ;
104- await api ( `/v1/messages/${ msg . id } ` , {
105- method : "PATCH" ,
106- body : JSON . stringify ( { content : newContent } ) ,
107- } ) ;
108- } ;
109-
85+ if ( ! loggedIn ) return < Navigate to = "/login" replace /> ;
11086 if ( ! workspaceId ) {
111- if ( workspaces . length > 0 ) {
112- return < Navigate to = { `/ws/${ workspaces [ 0 ] . id } ` } replace /> ;
113- }
114- return (
115- < div className = "flex-1 flex items-center justify-center text-gray-400" >
116- No workspaces available
117- </ div >
118- ) ;
87+ if ( workspaces . length > 0 ) return < Navigate to = { `/ws/${ workspaces [ 0 ] . id } ` } replace /> ;
88+ return < div style = { { flex : 1 , display : "flex" , alignItems : "center" , justifyContent : "center" , color : "#565B73" } } > No workspaces available</ div > ;
11989 }
12090
91+ // Check which sub-page to show
92+ const subPage = parts [ 2 ] ; // what comes after /ws/:id/
93+
94+ const renderContent = ( ) => {
95+ if ( subPage === "notifications" ) return < NotificationsPage /> ;
96+ if ( subPage === "calls" ) return < CallsPage /> ;
97+ if ( subPage === "workflows" ) return < WorkflowsPage /> ;
98+ if ( subPage === "billing" ) return < BillingPage /> ;
99+ if ( subPage === "integrations" ) return < IntegrationsPage /> ;
100+ if ( subPage === "settings" ) return < SettingsPage /> ;
101+ if ( subPage === "ch" && channel ) {
102+ return (
103+ < div style = { { display : "flex" , flex : 1 , minWidth : 0 } } >
104+ < ChannelView channel = { channel } messages = { messages }
105+ onSendMessage = { async ( c , ct ) => {
106+ if ( channel ) await api ( `/v1/channels/${ channel . id } /messages` , { method : "POST" , body : JSON . stringify ( { content : c , content_type : ct || "text" } ) } ) ;
107+ } }
108+ onEditMessage = { async ( msg ) => {
109+ const nc = prompt ( "Edit message:" , msg . content ) ;
110+ if ( nc !== null && nc !== msg . content ) await api ( `/v1/messages/${ msg . id } ` , { method : "PATCH" , body : JSON . stringify ( { content : nc } ) } ) ;
111+ } }
112+ onOpenThread = { ( id ) => setActiveThread ( id ) }
113+ />
114+ { activeThread && < ThreadPanel parentMessageId = { activeThread } onClose = { ( ) => setActiveThread ( null ) } /> }
115+ </ div >
116+ ) ;
117+ }
118+ // Default: try to show first channel
119+ if ( channels . length > 0 && ! subPage ) {
120+ return < Navigate to = { `/ws/${ workspaceId } /ch/${ channels [ 0 ] . id } ` } replace /> ;
121+ }
122+ return < div style = { { flex : 1 , display : "flex" , alignItems : "center" , justifyContent : "center" , color : "#565B73" } } > Select a channel</ div > ;
123+ } ;
124+
121125 return (
122- < div className = "flex h-screen bg-white" >
126+ < div style = { { display : "flex" , height : "100vh" , background : "#111318" } } >
123127 < Sidebar
124- workspaces = { workspaces }
125- workspace = { workspace }
126- channels = { channels }
127- channel = { channel }
128+ workspaces = { workspaces } workspace = { workspace } channels = { channels } channel = { channel }
128129 workspaceId = { workspaceId }
129- onSelectWorkspace = { handleSelectWorkspace }
130- onSelectChannel = { handleSelectChannel }
131- onLogout = { onLogout }
130+ onSelectWorkspace = { ( ws ) => navigate ( `/ws/ ${ ws . id } ` ) }
131+ onSelectChannel = { ( ch ) => navigate ( `/ws/ ${ workspaceId } /ch/ ${ ch . id } ` ) }
132+ onLogout = { ( ) => { logout ( ) ; navigate ( "/" ) ; } }
132133 />
133- < Routes >
134- < Route
135- path = "ch/:channelId"
136- element = {
137- channel ? (
138- < div className = "flex flex-1 min-w-0" >
139- < ChannelView
140- channel = { channel }
141- messages = { messages }
142- onSendMessage = { handleSendMessage }
143- onEditMessage = { handleEditMessage }
144- onOpenThread = { setActiveThread }
145- />
146- { activeThread && (
147- < ThreadPanel
148- parentMessageId = { activeThread }
149- onClose = { ( ) => setActiveThread ( null ) }
150- />
151- ) }
152- </ div >
153- ) : (
154- < div className = "flex-1 flex items-center justify-center text-gray-400" >
155- Select a channel
156- </ div >
157- )
158- }
159- />
160- < Route path = "notifications" element = { < NotificationsPage /> } />
161- < Route path = "calls" element = { < CallsPage /> } />
162- < Route path = "workflows" element = { < WorkflowsPage /> } />
163- < Route path = "billing" element = { < BillingPage /> } />
164- < Route path = "integrations" element = { < IntegrationsPage /> } />
165- < Route path = "settings" element = { < SettingsPage /> } />
166- < Route
167- index
168- element = {
169- channels . length > 0 ? (
170- < Navigate to = { `ch/${ channels [ 0 ] . id } ` } replace />
171- ) : (
172- < div className = "flex-1 flex items-center justify-center text-gray-400" >
173- No channels in this workspace
174- </ div >
175- )
176- }
177- />
178- </ Routes >
134+ { renderContent ( ) }
179135 </ div >
180136 ) ;
181137}
182138
183- export default function App ( ) {
184- const { loggedIn, login, logout } = useAuth ( ) ;
185- const navigate = useNavigate ( ) ;
139+ function AppRoot ( ) {
140+ const { loggedIn, login } = useAuth ( ) ;
186141
187- // Check for OAuth callback token in URL hash
188142 useEffect ( ( ) => {
189143 const hash = window . location . hash ;
190- if ( hash . startsWith ( "#token=" ) ) {
191- const token = hash . slice ( 7 ) ;
144+ if ( hash && hash . startsWith ( "#token=" ) ) {
145+ const t = hash . slice ( 7 ) ;
192146 window . location . hash = "" ;
193- login ( token ) . then ( ( ok ) => {
194- if ( ok ) {
195- api < Workspace [ ] > ( "/v1/workspaces" ) . then ( ( ws ) => {
196- if ( ws . length > 0 ) navigate ( `/ws/${ ws [ 0 ] . id } ` ) ;
197- } ) ;
198- }
147+ login ( t ) . then ( ( ok ) => {
148+ if ( ok ) api < Workspace [ ] > ( "/v1/workspaces" ) . then ( ( ws ) => { if ( ws . length > 0 ) window . location . href = `/ws/${ ws [ 0 ] . id } ` ; } ) ;
199149 } ) ;
200150 }
201- } , [ login , navigate ] ) ;
151+ } , [ login ] ) ;
152+
153+ if ( ! loggedIn ) {
154+ return < LandingPage /> ;
155+ }
156+
157+ return < WorkspaceLayout /> ;
158+ }
202159
160+ function LoginRoute ( ) {
161+ const { login } = useAuth ( ) ;
203162 const handleLogin = async ( token : string ) => {
204163 const ok = await login ( token ) ;
205164 if ( ok ) {
206165 const ws = await api < Workspace [ ] > ( "/v1/workspaces" ) ;
207- if ( ws . length > 0 ) navigate ( `/ws/${ ws [ 0 ] . id } ` ) ;
166+ if ( ws . length > 0 ) window . location . href = `/ws/${ ws [ 0 ] . id } ` ;
208167 }
209168 return ok ;
210169 } ;
170+ return < LoginView onLogin = { handleLogin } /> ;
171+ }
211172
212- const handleLogout = ( ) => {
213- logout ( ) ;
214- navigate ( "/" ) ;
215- } ;
216-
217- if ( ! loggedIn ) {
218- return < LoginView onLogin = { handleLogin } /> ;
219- }
173+ const router = createBrowserRouter ( [
174+ { path : "/" , element : < AppRoot /> } ,
175+ { path : "/login" , element : < LoginRoute /> } ,
176+ { path : "/ws" , element : < WorkspaceLayout /> } ,
177+ { path : "/ws/:workspaceId" , element : < WorkspaceLayout /> } ,
178+ { path : "/ws/:workspaceId/ch/:channelId" , element : < WorkspaceLayout /> } ,
179+ { path : "/ws/:workspaceId/notifications" , element : < WorkspaceLayout /> } ,
180+ { path : "/ws/:workspaceId/calls" , element : < WorkspaceLayout /> } ,
181+ { path : "/ws/:workspaceId/workflows" , element : < WorkspaceLayout /> } ,
182+ { path : "/ws/:workspaceId/billing" , element : < WorkspaceLayout /> } ,
183+ { path : "/ws/:workspaceId/integrations" , element : < WorkspaceLayout /> } ,
184+ { path : "/ws/:workspaceId/settings" , element : < WorkspaceLayout /> } ,
185+ { path : "*" , element : < Navigate to = "/" replace /> } ,
186+ ] ) ;
220187
221- return (
222- < Routes >
223- < Route path = "/ws/*" element = { < WorkspaceLayout loggedIn = { loggedIn } onLogout = { handleLogout } /> } />
224- < Route path = "*" element = { < Navigate to = "/ws" replace /> } />
225- </ Routes >
226- ) ;
188+ export default function App ( ) {
189+ return < RouterProvider router = { router } /> ;
227190}
0 commit comments