Skip to content

Commit 95f014d

Browse files
author
Hamid
committed
refactor(frontend): unify loading UI with shared Shimmer component
Replace ad-hoc Loader2 spinners and custom animate-pulse skeleton divs across accept-invite, product traceability, and user dashboard pages with the shared Shimmer component to standardize loading state styling and eliminate duplicated UI code. Removed unused Loader2 imports and the now-redundant custom SkeletonRow component.
1 parent 88a06a3 commit 95f014d

11 files changed

Lines changed: 83 additions & 61 deletions

File tree

frontend/src/app/accept-invite/page.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22

33
import { Suspense, useEffect, useState } from "react";
44
import { useRouter, useSearchParams } from "next/navigation";
5-
import { Loader2, Eye, EyeOff, CheckCircle2 } from "lucide-react";
5+
import { Eye, EyeOff, CheckCircle2 } from "lucide-react";
66
import { api, type UserRole } from "@/lib/api";
77
import { Button } from "@/components/ui/button";
88
import { Input, InputWrapper, InputLabel, InputError } from "@/components/ui/input";
99
import { useTranslation } from "@/i18n/hooks";
10+
import { Shimmer } from "@/components/shared/shimmer";
1011

1112
function AcceptInviteForm() {
1213
const { t } = useTranslation();
@@ -92,7 +93,7 @@ function AcceptInviteForm() {
9293
if (validating) {
9394
return (
9495
<div className="flex min-h-screen items-center justify-center">
95-
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
96+
<Shimmer className="h-6 w-6 rounded-full" />
9697
</div>
9798
);
9899
}
@@ -191,7 +192,7 @@ function AcceptInviteForm() {
191192

192193
<Button type="submit" className="w-full" disabled={submitting}>
193194
{submitting ? (
194-
<Loader2 className="h-4 w-4 animate-spin" />
195+
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
195196
) : (
196197
t("acceptInvite.submit")
197198
)}
@@ -207,11 +208,11 @@ export default function AcceptInvitePage() {
207208
<Suspense
208209
fallback={
209210
<div className="flex min-h-screen items-center justify-center">
210-
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
211+
<Shimmer className="h-6 w-6 rounded-full" />
211212
</div>
212213
}
213214
>
214215
<AcceptInviteForm />
215216
</Suspense>
216217
);
217-
}
218+
}

frontend/src/app/dashboard/products/[id]/traceability/page.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
} from "@/components/traceability/traceability-timeline";
99
import { AlertCircle, ArrowLeft, PackageSearch } from "lucide-react";
1010
import Link from "next/link";
11+
import { Shimmer } from "@/components/shared/shimmer";
1112

1213
// ─── Types ────────────────────────────────────────────────────────────────────
1314

@@ -56,8 +57,8 @@ function TraceabilitySkeleton() {
5657
<div className="space-y-6" aria-busy="true" aria-label="Loading traceability data">
5758
{/* Header skeleton */}
5859
<div className="space-y-2">
59-
<div className="h-7 w-48 animate-pulse rounded bg-muted" />
60-
<div className="h-4 w-32 animate-pulse rounded bg-muted" />
60+
<Shimmer className="h-7 w-48" />
61+
<Shimmer className="h-4 w-32" />
6162
</div>
6263

6364
{/* Timeline skeleton rows */}
@@ -67,11 +68,11 @@ function TraceabilitySkeleton() {
6768
{i < 4 && (
6869
<span className="absolute left-5 top-10 bottom-0 w-px bg-border" aria-hidden="true" />
6970
)}
70-
<div className="relative z-10 h-10 w-10 shrink-0 animate-pulse rounded-full bg-muted" />
71+
<Shimmer className="relative z-10 h-10 w-10 shrink-0 rounded-full" />
7172
<div className="flex-1 pb-8 space-y-2">
72-
<div className="h-4 w-40 animate-pulse rounded bg-muted" />
73-
<div className="h-3 w-56 animate-pulse rounded bg-muted" />
74-
<div className="h-3 w-24 animate-pulse rounded bg-muted" />
73+
<Shimmer className="h-4 w-40" />
74+
<Shimmer className="h-3 w-56" />
75+
<Shimmer className="h-3 w-24" />
7576
</div>
7677
</li>
7778
))}

frontend/src/app/dashboard/users/page.tsx

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { UserDetailDrawer } from "@/components/modules/user-detail-drawer";
3636
import { InviteUserDialog } from "@/components/modules/invite-user-dialog";
3737
import { Breadcrumbs } from "@/components/layout/breadcrumbs";
3838
import { useDebounce } from "@/lib/useDebounce";
39+
import { Shimmer } from "@/components/shared/shimmer";
3940

4041
function getInitials(name: string) {
4142
return name
@@ -54,17 +55,6 @@ function roleBadgeVariant(role: UserRole): BadgeProps["variant"] {
5455
}
5556
}
5657

57-
function SkeletonRow() {
58-
return (
59-
<TableRow>
60-
{[...Array(6)].map((_, i) => (
61-
<TableCell key={i}>
62-
<div className="h-4 animate-pulse rounded bg-muted" />
63-
</TableCell>
64-
))}
65-
</TableRow>
66-
);
67-
}
6858

6959
function StatCard({ label, value, color }: { label: string; value: number; color?: "emerald" | "red" | "amber" }) {
7060
const colorMap = { emerald: "text-emerald-500", red: "text-red-500", amber: "text-amber-500" };
@@ -217,7 +207,15 @@ export default function UsersPage() {
217207
</TableHeader>
218208
<TableBody>
219209
{loading ? (
220-
Array.from({ length: PAGE_LIMIT }).map((_, i) => <SkeletonRow key={i} />)
210+
Array.from({ length: PAGE_LIMIT }).map((_, i) => (
211+
<TableRow key={i}>
212+
{[...Array(6)].map((_, j) => (
213+
<TableCell key={j}>
214+
<Shimmer className="h-4 w-full" />
215+
</TableCell>
216+
))}
217+
</TableRow>
218+
))
221219
) : users.length === 0 ? (
222220
<TableRow>
223221
<TableCell colSpan={6} className="py-10 text-center text-sm text-muted-foreground">

frontend/src/components/dashboard/activity-feed.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { useQuery } from "@tanstack/react-query";
44
import { useActivityStream } from "@/lib/useActivityStream";
55
import { useState, useEffect } from "react";
6+
import { Shimmer } from "@/components/shared/shimmer";
67

78
interface ActivityItem {
89
id: string;
@@ -48,7 +49,7 @@ export function ActivityFeed() {
4849
return (
4950
<div className="space-y-2">
5051
{Array.from({ length: 4 }).map((_, i) => (
51-
<div key={i} className="h-4 w-full animate-pulse rounded bg-muted" />
52+
<Shimmer key={i} className="h-4 w-full" />
5253
))}
5354
</div>
5455
);

frontend/src/components/dashboard/dashboard-content.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
1616
import { Input } from "@/components/ui/input";
1717
import { Badge } from "@/components/ui/badge";
1818
import { EmptyState, ErrorState } from "@/components/shared/state-blocks";
19+
import { Shimmer } from "@/components/shared/shimmer";
1920
import { api } from "@/lib/api";
2021
import { cn } from "@/lib/utils";
2122

@@ -206,12 +207,24 @@ function DashboardSkeleton() {
206207
<div className="space-y-4">
207208
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-6">
208209
{Array.from({ length: 6 }).map((_, index) => (
209-
<div key={index} className="h-32 animate-pulse rounded-3xl bg-muted/30" />
210+
<div key={index} className="h-32 rounded-3xl border bg-card p-5">
211+
<div className="flex items-center justify-between">
212+
<Shimmer className="h-4 w-24" />
213+
<Shimmer className="h-8 w-8 rounded-xl" />
214+
</div>
215+
<div className="mt-4 space-y-2">
216+
<Shimmer className="h-7 w-20" />
217+
<Shimmer className="h-3 w-32" />
218+
</div>
219+
</div>
210220
))}
211221
</div>
212222
<div className="grid gap-4 xl:grid-cols-2">
213223
{Array.from({ length: 4 }).map((_, index) => (
214-
<div key={index} className="h-80 animate-pulse rounded-3xl bg-muted/30" />
224+
<div key={index} className="h-80 rounded-3xl border bg-card p-5">
225+
<Shimmer className="mb-4 h-4 w-28" />
226+
<Shimmer className="h-56 w-full" />
227+
</div>
215228
))}
216229
</div>
217230
</div>

frontend/src/components/landing/demo-showcase.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useState } from "react";
44
import Image from "next/image";
55
import { PlayCircle, Maximize2, Monitor } from "lucide-react";
66
import { useTranslation } from "@/i18n/hooks";
7+
import { Shimmer } from "@/components/shared/shimmer";
78

89
export function DemoShowcaseSection() {
910
const { t } = useTranslation();
@@ -76,12 +77,7 @@ export function DemoShowcaseSection() {
7677
{/* Loading shimmer */}
7778
{!isLoaded && (
7879
<div className="absolute inset-0 flex items-center justify-center">
79-
<div className="flex flex-col items-center gap-3">
80-
<div className="w-12 h-12 rounded-full border-2 border-primary/30 border-t-primary animate-spin" />
81-
<span className="text-xs text-muted-foreground/50">
82-
{t("landing.demoShowcase.loading")}
83-
</span>
84-
</div>
80+
<Shimmer className="w-12 h-12 rounded-full" />
8581
</div>
8682
)}
8783

@@ -108,7 +104,7 @@ export function DemoShowcaseSection() {
108104
</span>
109105
</div>
110106
<div className="flex items-center gap-1.5">
111-
<div className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" />
107+
<div className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
112108
<span className="text-[11px] text-muted-foreground/50">
113109
{t("landing.demoShowcase.liveLabel")}
114110
</span>

frontend/src/components/modules/analytics-module.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
2525
import { Input } from "@/components/ui/input";
2626
import { Badge } from "@/components/ui/badge";
2727
import { ErrorState } from "@/components/shared/state-blocks";
28+
import { Shimmer } from "@/components/shared/shimmer";
2829
import {
2930
Table,
3031
TableBody,
@@ -189,12 +190,24 @@ function AnalyticsSkeleton() {
189190
<div className="space-y-6">
190191
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
191192
{Array.from({ length: 4 }).map((_, i) => (
192-
<div key={i} className="h-28 animate-pulse rounded-3xl bg-muted/30" />
193+
<div key={i} className="h-28 rounded-3xl border bg-card p-5">
194+
<div className="flex items-center justify-between">
195+
<Shimmer className="h-4 w-24" />
196+
<Shimmer className="h-8 w-8 rounded-xl" />
197+
</div>
198+
<div className="mt-4 space-y-2">
199+
<Shimmer className="h-7 w-20" />
200+
<Shimmer className="h-3 w-32" />
201+
</div>
202+
</div>
193203
))}
194204
</div>
195205
<div className="grid gap-4 xl:grid-cols-2">
196206
{Array.from({ length: 4 }).map((_, i) => (
197-
<div key={i} className="h-72 animate-pulse rounded-3xl bg-muted/30" />
207+
<div key={i} className="h-72 rounded-3xl border bg-card p-5">
208+
<Shimmer className="mb-4 h-4 w-28" />
209+
<Shimmer className="h-56 w-full" />
210+
</div>
198211
))}
199212
</div>
200213
</div>

frontend/src/components/modules/invite-user-dialog.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { useState } from "react";
44
import { toast } from "sonner";
5-
import { Copy, Check, Loader2, UserPlus, Trash2, Clock } from "lucide-react";
5+
import { Copy, Check, UserPlus, Trash2, Clock } from "lucide-react";
66
import { api, type Invitation, type UserRole } from "@/lib/api";
77
import { useTranslation } from "@/i18n/hooks";
88
import {
@@ -24,6 +24,7 @@ import {
2424
} from "@/components/ui/select";
2525
import { Separator } from "@/components/ui/separator";
2626
import { Badge } from "@/components/ui/badge";
27+
import { Shimmer } from "@/components/shared/shimmer";
2728

2829
interface InviteUserDialogProps {
2930
open: boolean;
@@ -113,7 +114,7 @@ export function InviteUserDialog({ open, onClose }: InviteUserDialogProps) {
113114
</div>
114115
<div className="flex justify-end">
115116
<Button onClick={handleSend} disabled={sending} className="gap-1.5">
116-
{sending ? <Loader2 className="h-4 w-4 animate-spin" /> : <UserPlus className="h-4 w-4" />}
117+
{sending ? <span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" /> : <UserPlus className="h-4 w-4" />}
117118
{t("users.sendInvite")}
118119
</Button>
119120
</div>
@@ -135,7 +136,7 @@ export function InviteUserDialog({ open, onClose }: InviteUserDialogProps) {
135136
<Separator className="my-4" />
136137
<div className="space-y-2">
137138
<p className="text-sm font-semibold">{t("users.invitations")}</p>
138-
{loadingInvites ? <div className="h-8 animate-pulse rounded bg-muted" /> : (
139+
{loadingInvites ? <Shimmer className="h-8 w-full rounded" /> : (
139140
<ul className="space-y-2">
140141
{invitations.map((inv) => (
141142
<li key={inv.id} className="flex items-center justify-between gap-2 rounded-lg border border-border px-3 py-2">
@@ -150,7 +151,7 @@ export function InviteUserDialog({ open, onClose }: InviteUserDialogProps) {
150151
</div>
151152
</div>
152153
<Button size="sm" variant="ghost" onClick={() => handleRevoke(inv.id)} disabled={revoking === inv.id} className="shrink-0 text-muted-foreground hover:text-destructive" aria-label={t("users.revoke")}>
153-
{revoking === inv.id ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Trash2 className="h-3.5 w-3.5" />}
154+
{revoking === inv.id ? <span className="h-3.5 w-3.5 animate-spin rounded-full border-2 border-current border-t-transparent" /> : <Trash2 className="h-3.5 w-3.5" />}
154155
</Button>
155156
</li>
156157
))}

frontend/src/components/settings/notification-preferences-form.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useTranslation } from "@/i18n/hooks";
88
import { Checkbox } from "@/components/ui/checkbox";
99
import { Button } from "@/components/ui/button";
1010
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
11+
import { Shimmer } from "@/components/shared/shimmer";
1112

1213
type FormState = {
1314
certificateAlerts: boolean;
@@ -30,14 +31,14 @@ function PreferenceSkeleton() {
3031
{Array.from({ length: 4 }).map((_, i) => (
3132
<div key={i} className="flex items-center justify-between">
3233
<div className="space-y-1.5 flex-1">
33-
<div className="h-4 w-36 animate-pulse rounded bg-muted" />
34-
<div className="h-3 w-56 animate-pulse rounded bg-muted" />
34+
<Shimmer className="h-4 w-36" />
35+
<Shimmer className="h-3 w-56" />
3536
</div>
36-
<div className="h-5 w-5 animate-pulse rounded-full bg-muted shrink-0" />
37+
<Shimmer className="h-5 w-5 rounded-full shrink-0" />
3738
</div>
3839
))}
3940
<div className="flex justify-end pt-2">
40-
<div className="h-9 w-24 animate-pulse rounded bg-muted" />
41+
<Shimmer className="h-9 w-24" />
4142
</div>
4243
</CardContent>
4344
</Card>

frontend/src/components/settings/profile-section.tsx

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,31 +13,28 @@ import { Badge, type BadgeProps } from "@/components/ui/badge";
1313
import { Button } from "@/components/ui/button";
1414
import { Input, InputWrapper, InputLabel, InputError } from "@/components/ui/input";
1515
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
16+
import { Shimmer } from "@/components/shared/shimmer";
1617

17-
function SkeletonLine({ className = "" }: { className?: string }) {
18-
return <div className={`animate-pulse bg-muted rounded ${className}`} />;
19-
}
20-
21-
function ProfileSkeleton({ t }: { t: I18nContextValue["t"] }) {
18+
function ProfileSkeleton() {
2219
return (
2320
<div className="space-y-6">
2421
<Card>
2522
<CardContent className="pt-6">
2623
<div className="flex items-center gap-4">
27-
<SkeletonLine className="h-20 w-20 rounded-full" />
24+
<Shimmer className="h-20 w-20 rounded-full" />
2825
<div className="space-y-2">
29-
<SkeletonLine className="h-9 w-28" />
30-
<SkeletonLine className="h-9 w-24" />
26+
<Shimmer className="h-9 w-28" />
27+
<Shimmer className="h-9 w-24" />
3128
</div>
3229
</div>
3330
</CardContent>
3431
</Card>
3532
<Card>
3633
<CardContent className="pt-6 space-y-4">
37-
<div className="space-y-2"><SkeletonLine className="h-4 w-24" /><SkeletonLine className="h-[38px] w-full" /></div>
38-
<div className="space-y-2"><SkeletonLine className="h-4 w-16" /><SkeletonLine className="h-5 w-48" /></div>
39-
<div className="space-y-2"><SkeletonLine className="h-4 w-12" /><SkeletonLine className="h-6 w-20 rounded-full" /></div>
40-
<SkeletonLine className="h-[38px] w-32" />
34+
<div className="space-y-2"><Shimmer className="h-4 w-24" /><Shimmer className="h-[38px] w-full" /></div>
35+
<div className="space-y-2"><Shimmer className="h-4 w-16" /><Shimmer className="h-5 w-48" /></div>
36+
<div className="space-y-2"><Shimmer className="h-4 w-12" /><Shimmer className="h-6 w-20 rounded-full" /></div>
37+
<Shimmer className="h-[38px] w-32" />
4138
</CardContent>
4239
</Card>
4340
</div>
@@ -121,7 +118,7 @@ export function ProfileSection() {
121118

122119
const isSaveDisabled = isSubmitting || currentName === savedName || !profile;
123120

124-
if (loading) return <ProfileSkeleton t={t} />;
121+
if (loading) return <ProfileSkeleton />;
125122

126123
if (error) {
127124
return (

0 commit comments

Comments
 (0)