11// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22
3- import { createFileRoute , Link , useNavigate } from '@tanstack/react-router' ;
4- import { Building2 , Check , Plus } from 'lucide-react' ;
3+ import { createFileRoute , useNavigate } from '@tanstack/react-router' ;
4+ import { Building2 , Check , Plus , Settings } from 'lucide-react' ;
55import { Button } from '@/components/ui/button' ;
6- import { Card , CardContent , CardDescription , CardHeader , CardTitle } from '@/components/ui/card' ;
6+ import { Card } from '@/components/ui/card' ;
7+ import { Badge } from '@/components/ui/badge' ;
78import { toast } from '@/hooks/use-toast' ;
89import { useOrganizations , useSession } from '@/hooks/useSession' ;
910
@@ -12,19 +13,20 @@ export const Route = createFileRoute('/orgs/')({
1213} ) ;
1314
1415function OrgsListPage ( ) {
15- const { organizations, loading, reload } = useOrganizations ( ) ;
16+ const { organizations, loading } = useOrganizations ( ) ;
1617 const { session, setActiveOrganization } = useSession ( ) ;
1718 const navigate = useNavigate ( ) ;
1819 const activeId = session ?. activeOrganizationId ?? undefined ;
1920
20- const handleSetActive = async ( id : string ) => {
21+ const handleSelect = async ( id : string ) => {
2122 try {
22- await setActiveOrganization ( id ) ;
23- await reload ( ) ;
24- toast ( { title : 'Organization switched' } ) ;
23+ if ( id !== activeId ) {
24+ await setActiveOrganization ( id ) ;
25+ }
26+ navigate ( { to : '/projects' } ) ;
2527 } catch ( err ) {
2628 toast ( {
27- title : 'Failed to switch' ,
29+ title : 'Failed to switch organization ' ,
2830 description : ( err as Error ) . message ,
2931 variant : 'destructive' ,
3032 } ) ;
@@ -39,58 +41,84 @@ function OrgsListPage() {
3941 < div >
4042 < h1 className = "text-2xl font-semibold" > Organizations</ h1 >
4143 < p className = "text-sm text-muted-foreground" >
42- Manage the organizations you belong to .
44+ Select an organization to work with, or create a new one .
4345 </ p >
4446 </ div >
4547 < Button onClick = { ( ) => navigate ( { to : '/orgs/new' } ) } >
4648 < Plus className = "mr-2 h-4 w-4" /> New organization
4749 </ Button >
4850 </ div >
51+
4952 { loading && < p className = "text-sm text-muted-foreground" > Loading…</ p > }
53+
5054 { ! loading && organizations . length === 0 && (
51- < Card >
52- < CardContent className = "flex flex-col items-center gap-3 py-12 text-center" >
53- < Building2 className = "h-10 w-10 text-muted-foreground" / >
54- < p className = "text-sm text-muted-foreground" >
55- You don't belong to any organization yet .
56- </ p >
57- < Button onClick = { ( ) => navigate ( { to : '/orgs/new' } ) } >
58- Create your first organization
59- </ Button >
60- </ CardContent >
55+ < Card className = "p-10 text-center" >
56+ < Building2 className = "mx-auto mb-3 h-10 w-10 text-muted-foreground" / >
57+ < h3 className = "text-base font-medium" > No organizations yet </ h3 >
58+ < p className = "mb-4 text-sm text-muted-foreground" >
59+ Create your first organization to start building .
60+ </ p >
61+ < Button onClick = { ( ) => navigate ( { to : '/orgs/new' } ) } >
62+ < Plus className = "mr-2 h-4 w-4" />
63+ Create organization
64+ </ Button >
6165 </ Card >
6266 ) }
67+
6368 < div className = "grid gap-3" >
64- { organizations . map ( ( org ) => (
65- < Card key = { org . id } >
66- < CardHeader className = "flex flex-row items-center justify-between space-y-0 pb-2" >
67- < div >
68- < CardTitle className = "text-base" > { org . name } </ CardTitle >
69- { org . slug && (
70- < CardDescription className = "font-mono text-xs" > { org . slug } </ CardDescription >
71- ) }
72- </ div >
73- { org . id === activeId ? (
74- < span className = "flex items-center gap-1 text-xs text-primary" >
75- < Check className = "h-3.5 w-3.5" /> Active
76- </ span >
77- ) : (
78- < Button size = "sm" variant = "outline" onClick = { ( ) => handleSetActive ( org . id ) } >
79- Set active
69+ { organizations . map ( ( org ) => {
70+ const isActive = org . id === activeId ;
71+ return (
72+ < Card
73+ key = { org . id }
74+ role = "button"
75+ tabIndex = { 0 }
76+ onClick = { ( ) => handleSelect ( org . id ) }
77+ onKeyDown = { ( e ) => {
78+ if ( e . key === 'Enter' || e . key === ' ' ) {
79+ e . preventDefault ( ) ;
80+ handleSelect ( org . id ) ;
81+ }
82+ } }
83+ className = { `cursor-pointer p-4 transition-colors hover:bg-accent focus:outline-none focus:ring-2 focus:ring-ring ${
84+ isActive ? 'border-primary ring-1 ring-primary/40' : ''
85+ } `}
86+ >
87+ < div className = "flex items-start justify-between gap-4" >
88+ < div className = "min-w-0 flex-1" >
89+ < div className = "flex items-center gap-2" >
90+ < h3 className = "truncate text-base font-medium" >
91+ { org . name }
92+ </ h3 >
93+ { isActive && (
94+ < Badge variant = "outline" className = "gap-1 text-[10px]" >
95+ < Check className = "h-3 w-3" />
96+ Active
97+ </ Badge >
98+ ) }
99+ </ div >
100+ { org . slug && (
101+ < code className = "mt-1 block font-mono text-xs text-muted-foreground" >
102+ { org . slug }
103+ </ code >
104+ ) }
105+ </ div >
106+ < Button
107+ variant = "ghost"
108+ size = "sm"
109+ className = "h-8 w-8 p-0"
110+ onClick = { ( e ) => {
111+ e . stopPropagation ( ) ;
112+ navigate ( { to : '/orgs/$orgId' , params : { orgId : org . id } } ) ;
113+ } }
114+ aria-label = "Organization settings"
115+ >
116+ < Settings className = "h-4 w-4" />
80117 </ Button >
81- ) }
82- </ CardHeader >
83- < CardContent className = "pt-0" >
84- < Link
85- to = "/orgs/$orgId"
86- params = { { orgId : org . id } }
87- className = "text-xs text-primary hover:underline"
88- >
89- View details →
90- </ Link >
91- </ CardContent >
92- </ Card >
93- ) ) }
118+ </ div >
119+ </ Card >
120+ ) ;
121+ } ) }
94122 </ div >
95123 </ div >
96124 </ div >
0 commit comments