Skip to content

Commit a51e2a6

Browse files
committed
refactor: organizations list & interaction
1 parent f2b1994 commit a51e2a6

File tree

4 files changed

+190
-198
lines changed

4 files changed

+190
-198
lines changed
Lines changed: 55 additions & 180 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,17 @@
11
'use client';
22

3+
import { authClient } from '@databuddy/auth/client';
34
import {
4-
ArrowRightIcon,
55
BuildingsIcon,
66
CalendarIcon,
77
CheckIcon,
88
GearIcon,
9-
TrashIcon,
109
} from '@phosphor-icons/react';
1110
import dayjs from 'dayjs';
1211
import relativeTime from 'dayjs/plugin/relativeTime';
13-
import Link from 'next/link';
12+
import { useRouter } from 'next/navigation';
1413
import { useState } from 'react';
1514
import { toast } from 'sonner';
16-
import {
17-
AlertDialog,
18-
AlertDialogAction,
19-
AlertDialogCancel,
20-
AlertDialogContent,
21-
AlertDialogDescription,
22-
AlertDialogFooter,
23-
AlertDialogHeader,
24-
AlertDialogTitle,
25-
} from '@/components/ui/alert-dialog';
2615
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
2716
import { Badge } from '@/components/ui/badge';
2817
import { Button } from '@/components/ui/button';
@@ -34,42 +23,31 @@ import {
3423
TooltipProvider,
3524
TooltipTrigger,
3625
} from '@/components/ui/tooltip';
37-
import {
38-
type ActiveOrganization,
39-
type Organization,
40-
useOrganizations,
41-
} from '@/hooks/use-organizations';
4226
import { cn, getOrganizationInitials } from '@/lib/utils';
4327
import { EmptyState } from './empty-state';
4428

4529
dayjs.extend(relativeTime);
4630

4731
interface OrganizationsListProps {
48-
organizations: Organization[];
49-
activeOrganization: ActiveOrganization;
32+
organizations: ReturnType<typeof authClient.useListOrganizations>['data'];
33+
activeOrganization: ReturnType<typeof authClient.useActiveOrganization>['data'];
5034
isLoading: boolean;
5135
}
5236

5337
function OrganizationSkeleton() {
5438
return (
5539
<Card className="group relative overflow-hidden">
56-
<CardContent className="p-4 sm:p-6">
57-
<div className="space-y-4 sm:space-y-6">
58-
<div className="flex items-start gap-3 sm:gap-4">
59-
<Skeleton className="h-10 w-10 flex-shrink-0 rounded-full sm:h-12 sm:w-12" />
60-
<div className="min-w-0 flex-1 space-y-2">
61-
<Skeleton className="h-3 w-28 sm:h-4 sm:w-32" />
62-
<Skeleton className="h-3 w-20 sm:h-3 sm:w-24" />
63-
<Skeleton className="h-3 w-24 sm:h-3 sm:w-28" />
64-
</div>
65-
</div>
66-
<div className="space-y-3">
67-
<Skeleton className="h-9 w-full sm:h-10" />
68-
<div className="flex gap-2 sm:gap-3">
69-
<Skeleton className="h-9 flex-1 sm:h-10" />
70-
<Skeleton className="h-9 w-9 sm:h-10 sm:w-10" />
40+
<CardContent className="p-4">
41+
<div className="space-y-3">
42+
<div className="flex items-start gap-3">
43+
<Skeleton className="h-10 w-10 flex-shrink-0 rounded-full" />
44+
<div className="min-w-0 flex-1 space-y-1.5">
45+
<Skeleton className="h-3 w-32" />
46+
<Skeleton className="h-3 w-24" />
47+
<Skeleton className="h-3 w-28" />
7148
</div>
7249
</div>
50+
<Skeleton className="h-3 w-40" />
7351
</div>
7452
</CardContent>
7553
</Card>
@@ -96,38 +74,30 @@ export function OrganizationsList({
9674
activeOrganization,
9775
isLoading,
9876
}: OrganizationsListProps) {
99-
const {
100-
setActiveOrganization,
101-
deleteOrganizationAsync,
102-
isSettingActiveOrganization,
103-
isDeletingOrganization,
104-
} = useOrganizations();
105-
const [deletingId, setDeletingId] = useState<string | null>(null);
106-
const [confirmDelete, setConfirmDelete] = useState<{
107-
id: string;
108-
name: string;
109-
} | null>(null);
110-
111-
const handleSetActive = (organizationId: string) => {
112-
setActiveOrganization(organizationId);
113-
};
114-
115-
const handleDelete = (organizationId: string, organizationName: string) => {
116-
setConfirmDelete({ id: organizationId, name: organizationName });
117-
};
118-
119-
const confirmDeleteAction = async () => {
120-
if (!confirmDelete) {
77+
const router = useRouter();
78+
const [processingId, setProcessingId] = useState<string | null>(null);
79+
80+
const handleCardClick = async (orgId: string) => {
81+
const isCurrentlyActive = activeOrganization?.id === orgId;
82+
83+
if (isCurrentlyActive) {
84+
router.push('/organizations/settings');
12185
return;
12286
}
123-
setDeletingId(confirmDelete.id);
87+
88+
setProcessingId(orgId);
12489
try {
125-
await deleteOrganizationAsync(confirmDelete.id);
90+
const { error } = await authClient.organization.setActive({ organizationId: orgId });
91+
if (error) {
92+
toast.error(error.message || 'Failed to switch workspace');
93+
} else {
94+
toast.success('Workspace updated');
95+
router.push('/organizations/settings');
96+
}
12697
} catch (_error) {
127-
toast.error('Failed to delete organization');
98+
toast.error('Failed to switch workspace');
12899
} finally {
129-
setDeletingId(null);
130-
setConfirmDelete(null);
100+
setProcessingId(null);
131101
}
132102
};
133103

@@ -150,22 +120,24 @@ export function OrganizationsList({
150120
return (
151121
<div className="p-4 sm:p-6">
152122
<div className="grid grid-cols-1 gap-4 sm:gap-6 sm:grid-cols-2 lg:grid-cols-3">
153-
{organizations.map((org) => {
123+
{organizations?.map((org) => {
154124
const isActive = activeOrganization?.id === org.id;
155-
const isDeleting = deletingId === org.id;
125+
const isProcessing = processingId === org.id;
156126

157127
return (
158128
<Card
159129
className={cn(
160-
'group relative overflow-hidden transition-all duration-200 hover:shadow-sm',
130+
'group relative cursor-pointer overflow-hidden transition-all duration-200',
161131
isActive
162132
? 'border-primary/30 bg-primary/5 shadow-sm'
163-
: 'hover:border-border/60 hover:bg-muted/30'
133+
: 'hover:border-border/60 hover:bg-muted/30',
134+
isProcessing && 'opacity-70 pointer-events-none'
164135
)}
165136
key={org.id}
137+
onClick={() => handleCardClick(org.id)}
166138
>
167139
{isActive && (
168-
<div className="absolute top-2 right-2">
140+
<div className="absolute top-3 right-3">
169141
<Badge
170142
className="border-primary/20 bg-primary/10 text-primary text-xs"
171143
variant="secondary"
@@ -176,143 +148,46 @@ export function OrganizationsList({
176148
</div>
177149
)}
178150

179-
<CardContent className="p-4 sm:p-6">
180-
<div className="space-y-4 sm:space-y-6">
151+
{isProcessing && (
152+
<div className="absolute inset-0 flex items-center justify-center bg-background/50 backdrop-blur-sm">
153+
<div className="h-6 w-6 animate-spin rounded-full border border-primary/30 border-t-primary" />
154+
</div>
155+
)}
156+
157+
<CardContent className="p-4">
158+
<div className="space-y-3">
181159
{/* Organization Info */}
182-
<div className="flex items-start gap-3 sm:gap-4">
183-
<Avatar className="h-10 w-10 flex-shrink-0 border border-border/30 sm:h-12 sm:w-12">
160+
<div className="flex items-start gap-3">
161+
<Avatar className="h-10 w-10 flex-shrink-0 border border-border/30">
184162
<AvatarImage alt={org.name} src={org.logo || undefined} />
185-
<AvatarFallback className="bg-accent font-medium text-xs sm:text-sm">
163+
<AvatarFallback className="bg-accent font-medium text-xs">
186164
{getOrganizationInitials(org.name)}
187165
</AvatarFallback>
188166
</Avatar>
189167
<div className="min-w-0 flex-1">
190-
<h3 className="truncate font-semibold text-sm sm:text-base">
168+
<h3 className="truncate font-semibold text-sm">
191169
{org.name}
192170
</h3>
193-
<p className="truncate text-muted-foreground text-xs sm:text-sm">
171+
<p className="truncate text-muted-foreground text-xs">
194172
@{org.slug}
195173
</p>
196-
<div className="mt-1 flex items-center gap-1 sm:mt-2">
174+
<div className="mt-0.5 flex items-center gap-1">
197175
<CalendarIcon
198-
className="h-3 w-3 text-muted-foreground sm:h-4 sm:w-4"
176+
className="h-3 w-3 text-muted-foreground"
199177
size={12}
200178
/>
201-
<span className="text-muted-foreground text-xs sm:text-sm">
179+
<span className="text-muted-foreground text-xs">
202180
Created {dayjs(org.createdAt).fromNow()}
203181
</span>
204182
</div>
205183
</div>
206184
</div>
207-
208-
{/* Actions */}
209-
<div className="space-y-3">
210-
{isActive ? (
211-
<Button
212-
className="h-9 w-full rounded font-medium sm:h-10"
213-
disabled
214-
variant="secondary"
215-
>
216-
<CheckIcon className="mr-2 h-3 w-3 sm:h-4 sm:w-4" size={12} />
217-
<span className="text-xs sm:text-sm">Current Organization</span>
218-
</Button>
219-
) : (
220-
<Button
221-
className="h-9 w-full rounded font-medium sm:h-10"
222-
disabled={isSettingActiveOrganization}
223-
onClick={() => handleSetActive(org.id)}
224-
>
225-
{isSettingActiveOrganization ? (
226-
<>
227-
<div className="mr-2 h-3 w-3 animate-spin rounded-full border border-primary-foreground/30 border-t-primary-foreground sm:h-4 sm:w-4" />
228-
<span className="text-xs sm:text-sm">Switching...</span>
229-
</>
230-
) : (
231-
<>
232-
<ArrowRightIcon
233-
className="mr-2 h-3 w-3 sm:h-4 sm:w-4"
234-
size={12}
235-
/>
236-
<span className="text-xs sm:text-sm">Switch Organization</span>
237-
</>
238-
)}
239-
</Button>
240-
)}
241-
242-
<div className="flex items-center gap-2 sm:gap-3">
243-
<TooltipProvider>
244-
<Tooltip>
245-
<TooltipTrigger asChild>
246-
<Button
247-
asChild
248-
className="h-9 flex-1 rounded font-medium sm:h-10"
249-
variant="outline"
250-
>
251-
<Link href="/organizations/settings">
252-
<GearIcon className="mr-2 h-3 w-3 sm:h-4 sm:w-4" size={12} />
253-
<span className="text-xs sm:text-sm">Settings</span>
254-
</Link>
255-
</Button>
256-
</TooltipTrigger>
257-
<TooltipContent side="bottom">
258-
<p>Manage organization settings</p>
259-
</TooltipContent>
260-
</Tooltip>
261-
</TooltipProvider>
262-
263-
<TooltipProvider>
264-
<Tooltip>
265-
<TooltipTrigger asChild>
266-
<Button
267-
className="h-9 w-9 rounded p-0 hover:border-destructive/30 hover:bg-destructive/10 hover:text-destructive sm:h-10 sm:w-10"
268-
disabled={isDeleting || isDeletingOrganization}
269-
onClick={() => handleDelete(org.id, org.name)}
270-
variant="outline"
271-
>
272-
{isDeleting ? (
273-
<div className="h-3 w-3 animate-spin rounded-full border border-destructive/30 border-t-destructive sm:h-4 sm:w-4" />
274-
) : (
275-
<TrashIcon className="h-3 w-3 sm:h-4 sm:w-4" size={12} />
276-
)}
277-
</Button>
278-
</TooltipTrigger>
279-
<TooltipContent side="bottom">
280-
<p>Delete organization</p>
281-
</TooltipContent>
282-
</Tooltip>
283-
</TooltipProvider>
284-
</div>
285-
</div>
286185
</div>
287186
</CardContent>
288187
</Card>
289188
);
290189
})}
291190
</div>
292-
293-
<AlertDialog
294-
onOpenChange={(open) => !open && setConfirmDelete(null)}
295-
open={!!confirmDelete}
296-
>
297-
<AlertDialogContent>
298-
<AlertDialogHeader>
299-
<AlertDialogTitle>Delete organization</AlertDialogTitle>
300-
<AlertDialogDescription>
301-
This will permanently delete "{confirmDelete?.name}" and all
302-
associated resources. This action cannot be undone.
303-
</AlertDialogDescription>
304-
</AlertDialogHeader>
305-
<AlertDialogFooter>
306-
<AlertDialogCancel>Cancel</AlertDialogCancel>
307-
<AlertDialogAction
308-
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
309-
onClick={confirmDeleteAction}
310-
>
311-
Delete
312-
</AlertDialogAction>
313-
</AlertDialogFooter>
314-
</AlertDialogContent>
315-
</AlertDialog>
316191
</div>
317192
);
318193
}

apps/dashboard/app/(main)/organizations/page.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
'use client';
22

3+
import { authClient } from '@databuddy/auth/client';
34
import { Suspense } from 'react';
45
import { Card, CardContent } from '@/components/ui/card';
56
import { Skeleton } from '@/components/ui/skeleton';
6-
import { useOrganizations } from '@/hooks/use-organizations';
77
import { OrganizationsList } from './components/organizations-list';
88

99
function OrganizationsSkeleton() {
@@ -37,7 +37,12 @@ function OrganizationsSkeleton() {
3737
}
3838

3939
export default function OrganizationsPage() {
40-
const { organizations, activeOrganization, isLoading } = useOrganizations();
40+
const { data: organizations, isPending: isOrganizationsPending } =
41+
authClient.useListOrganizations();
42+
const { data: activeOrganization, isPending: isActiveOrganizationPending } =
43+
authClient.useActiveOrganization();
44+
45+
const isLoading = isOrganizationsPending || isActiveOrganizationPending;
4146

4247
return (
4348
<div className="flex h-full flex-col">

0 commit comments

Comments
 (0)