refactor(frontend): Replace LoadingSpinner with shadcn skeleton components.

- Created AccountsSkeleton.tsx and NotificationsSkeleton.tsx components
- Updated AccountsOverview.tsx and Notifications.tsx to use skeletons
- Removed unused LoadingSpinner.tsx component
- Improved loading state UX by showing content structure

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Elisiário Couto
2025-09-16 18:23:50 +01:00
committed by Elisiário Couto
parent fb310a5953
commit 84e609a774
13 changed files with 406 additions and 265 deletions

View File

@@ -22,7 +22,7 @@ import {
} from "./ui/card";
import { Button } from "./ui/button";
import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
import LoadingSpinner from "./LoadingSpinner";
import AccountsSkeleton from "./AccountsSkeleton";
import type { Account, Balance } from "../types/api";
// Helper function to get status indicator color and styles
@@ -114,11 +114,7 @@ export default function AccountsOverview() {
};
if (accountsLoading) {
return (
<Card>
<LoadingSpinner message="Loading accounts..." />
</Card>
);
return <AccountsSkeleton />;
}
if (accountsError) {
@@ -304,7 +300,9 @@ export default function AccountsOverview() {
<div>
<div className="flex items-center space-x-2 min-w-0">
<h4 className="text-base sm:text-lg font-medium text-foreground truncate">
{account.display_name || account.name || "Unnamed Account"}
{account.display_name ||
account.name ||
"Unnamed Account"}
</h4>
<button
onClick={() => handleEditStart(account)}

View File

@@ -0,0 +1,61 @@
import { Skeleton } from "./ui/skeleton";
import { Card, CardContent, CardHeader } from "./ui/card";
export default function AccountsSkeleton() {
return (
<div className="space-y-6">
{/* Summary Cards Skeleton */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{Array.from({ length: 3 }).map((_, i) => (
<Card key={i}>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div className="space-y-2">
<Skeleton className="h-4 w-20" />
<Skeleton className="h-8 w-24" />
</div>
<Skeleton className="h-12 w-12 rounded-full" />
</div>
</CardContent>
</Card>
))}
</div>
{/* Accounts List Skeleton */}
<Card>
<CardHeader>
<Skeleton className="h-6 w-32" />
<Skeleton className="h-4 w-48" />
</CardHeader>
<CardContent className="p-0">
<div className="divide-y divide-border">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="p-4 sm:p-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
<div className="flex items-start sm:items-center space-x-3 sm:space-x-4 min-w-0 flex-1">
<Skeleton className="h-10 w-10 sm:h-12 sm:w-12 rounded-full flex-shrink-0" />
<div className="flex-1 space-y-2">
<Skeleton className="h-5 w-48" />
<Skeleton className="h-4 w-32" />
<Skeleton className="h-3 w-40" />
</div>
</div>
<div className="flex items-center justify-between sm:flex-col sm:items-end sm:text-right flex-shrink-0">
<div className="flex items-center space-x-2 order-1 sm:order-2">
<Skeleton className="h-3 w-3 rounded-full" />
<Skeleton className="h-4 w-20" />
</div>
<div className="flex items-center space-x-2 order-2 sm:order-1">
<Skeleton className="h-4 w-4" />
<Skeleton className="h-5 w-24" />
</div>
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -1,21 +0,0 @@
import { RefreshCw } from "lucide-react";
import { cn } from "../lib/utils";
interface LoadingSpinnerProps {
message?: string;
className?: string;
}
export default function LoadingSpinner({
message = "Loading...",
className,
}: LoadingSpinnerProps) {
return (
<div className={cn("flex items-center justify-center p-8", className)}>
<div className="text-center">
<RefreshCw className="h-8 w-8 animate-spin text-primary mx-auto mb-2" />
<p className="text-muted-foreground text-sm">{message}</p>
</div>
</div>
);
}

View File

@@ -12,7 +12,7 @@ import {
TestTube,
} from "lucide-react";
import { apiClient } from "../lib/api";
import LoadingSpinner from "./LoadingSpinner";
import NotificationsSkeleton from "./NotificationsSkeleton";
import {
Card,
CardContent,
@@ -81,11 +81,7 @@ export default function Notifications() {
});
if (settingsLoading || servicesLoading) {
return (
<Card>
<LoadingSpinner message="Loading notifications..." />
</Card>
);
return <NotificationsSkeleton />;
}
if (settingsError || servicesError) {
@@ -235,7 +231,9 @@ export default function Notifications() {
</h4>
<div className="flex items-center space-x-2 mt-1">
<Badge
variant={service.enabled ? "default" : "destructive"}
variant={
service.enabled ? "default" : "destructive"
}
>
{service.enabled ? (
<CheckCircle className="h-3 w-3 mr-1" />
@@ -245,7 +243,9 @@ export default function Notifications() {
{service.enabled ? "Enabled" : "Disabled"}
</Badge>
<Badge
variant={service.configured ? "secondary" : "outline"}
variant={
service.configured ? "secondary" : "outline"
}
>
{service.configured
? "Configured"

View File

@@ -0,0 +1,95 @@
import { Skeleton } from "./ui/skeleton";
import { Card, CardContent, CardHeader } from "./ui/card";
export default function NotificationsSkeleton() {
return (
<div className="space-y-6">
{/* Test Notification Section Skeleton */}
<Card>
<CardHeader>
<div className="flex items-center space-x-2">
<Skeleton className="h-5 w-5" />
<Skeleton className="h-6 w-36" />
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Skeleton className="h-4 w-16" />
<Skeleton className="h-10 w-full" />
</div>
<div className="space-y-2">
<Skeleton className="h-4 w-16" />
<Skeleton className="h-10 w-full" />
</div>
</div>
<div className="mt-4">
<Skeleton className="h-10 w-48" />
</div>
</CardContent>
</Card>
{/* Notification Services Skeleton */}
<Card>
<CardHeader>
<div className="flex items-center space-x-2">
<Skeleton className="h-5 w-5" />
<Skeleton className="h-6 w-40" />
</div>
<Skeleton className="h-4 w-56" />
</CardHeader>
<CardContent className="p-0">
<div className="divide-y divide-border">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="p-6">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<Skeleton className="h-12 w-12 rounded-full" />
<div className="space-y-2">
<Skeleton className="h-5 w-24" />
<div className="flex items-center space-x-2">
<Skeleton className="h-5 w-16" />
<Skeleton className="h-5 w-20" />
</div>
</div>
</div>
<Skeleton className="h-8 w-8" />
</div>
</div>
))}
</div>
</CardContent>
</Card>
{/* Notification Settings Skeleton */}
<Card>
<CardHeader>
<div className="flex items-center space-x-2">
<Skeleton className="h-5 w-5" />
<Skeleton className="h-6 w-40" />
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="space-y-2">
<Skeleton className="h-4 w-16" />
<div className="bg-muted rounded-md p-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2">
<Skeleton className="h-3 w-32" />
<Skeleton className="h-4 w-24" />
</div>
<div className="space-y-2">
<Skeleton className="h-3 w-28" />
<Skeleton className="h-4 w-20" />
</div>
</div>
</div>
</div>
<Skeleton className="h-12 w-full" />
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -38,7 +38,8 @@ export function AccountCombobox({
);
const formatAccountName = (account: Account) => {
const displayName = account.display_name || account.name || "Unnamed Account";
const displayName =
account.display_name || account.name || "Unnamed Account";
return `${displayName} (${account.institution_id})`;
};
@@ -105,7 +106,9 @@ export function AccountCombobox({
/>
<div className="flex flex-col">
<span className="font-medium">
{account.display_name || account.name || "Unnamed Account"}
{account.display_name ||
account.name ||
"Unnamed Account"}
</span>
<span className="text-xs text-gray-500">
{account.institution_id}

View File

@@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
{
variants: {
variant: {

View File

@@ -11,7 +11,7 @@ const Progress = React.forwardRef<
ref={ref}
className={cn(
"relative h-2 w-full overflow-hidden rounded-full bg-secondary",
className
className,
)}
{...props}
>
@@ -23,4 +23,4 @@ const Progress = React.forwardRef<
));
Progress.displayName = ProgressPrimitive.Root.displayName;
export { Progress };
export { Progress };

View File

@@ -20,7 +20,7 @@ const SheetOverlay = React.forwardRef<
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
className,
)}
{...props}
ref={ref}
@@ -44,7 +44,7 @@ const sheetVariants = cva(
defaultVariants: {
side: "right",
},
}
},
);
interface SheetContentProps
@@ -79,7 +79,7 @@ const SheetHeader = ({
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
className,
)}
{...props}
/>
@@ -93,7 +93,7 @@ const SheetFooter = ({
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
className,
)}
{...props}
/>
@@ -135,4 +135,4 @@ export {
SheetFooter,
SheetTitle,
SheetDescription,
};
};

View File

@@ -12,4 +12,4 @@ function Skeleton({
);
}
export { Skeleton };
export { Skeleton };

View File

@@ -24,4 +24,4 @@ const Toaster = ({ ...props }: ToasterProps) => {
);
};
export { Toaster };
export { Toaster };

View File

@@ -42,10 +42,9 @@ export const apiClient = {
id: string,
updates: AccountUpdate,
): Promise<{ id: string; display_name?: string }> => {
const response = await api.put<ApiResponse<{ id: string; display_name?: string }>>(
`/accounts/${id}`,
updates,
);
const response = await api.put<
ApiResponse<{ id: string; display_name?: string }>
>(`/accounts/${id}`, updates);
return response.data.data;
},