Skip to content

Commit a095b7c

Browse files
authored
Merge pull request Stellar-IndigoPay#341 from Olasunkanmi975/feat/frontend-page-skeleton-loading
feat(frontend): add page-specific skeleton loading states to reduce CLS
2 parents ae737da + 8b644ba commit a095b7c

14 files changed

Lines changed: 679 additions & 163 deletions
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/**
2+
* components/DashboardSkeleton.tsx
3+
*
4+
* Loading placeholder that mirrors the dashboard layout:
5+
* - header ("My Impact" + wallet pill + Donate button)
6+
* - stats grid (4 stat cards)
7+
* - tabbed sections: impact certificate, streak, donation history
8+
*
9+
* Uses only the existing primitives in `components/Skeleton.tsx`.
10+
*/
11+
import {
12+
SkeletonBox,
13+
SkeletonText,
14+
SkeletonStatCard,
15+
SkeletonAvatar,
16+
SkeletonBadge,
17+
} from "@/components/Skeleton";
18+
19+
export default function DashboardSkeleton() {
20+
return (
21+
<div className="max-w-5xl mx-auto px-4 sm:px-6 py-10 animate-pulse pointer-events-none">
22+
{/* Header */}
23+
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-8">
24+
<div>
25+
<SkeletonBox className="h-9 rounded w-44 mb-2" palette="indigo" />
26+
<div className="flex items-center gap-2">
27+
<span className="w-1.5 h-1.5 rounded-full bg-forest-300" />
28+
<SkeletonBox className="h-3 rounded w-24" palette="indigo" />
29+
</div>
30+
</div>
31+
<SkeletonBox className="h-10 rounded-xl w-36" palette="indigo" />
32+
</div>
33+
34+
{/* Stats grid */}
35+
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
36+
{[1, 2, 3, 4].map((i) => (
37+
<SkeletonStatCard key={i} palette="indigo" />
38+
))}
39+
</div>
40+
41+
{/* Tab strip */}
42+
<div className="flex gap-2 mb-8">
43+
{[1, 2].map((i) => (
44+
<SkeletonBox
45+
key={i}
46+
className="h-9 rounded-lg w-24"
47+
palette="indigo"
48+
/>
49+
))}
50+
</div>
51+
52+
{/* Impact Certificate card */}
53+
<div className="card mb-8">
54+
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
55+
<div className="flex-1">
56+
<SkeletonBox className="h-6 rounded w-56 mb-2" palette="indigo" />
57+
<SkeletonText lines={2} className="max-w-md" palette="indigo" />
58+
</div>
59+
<SkeletonBox className="h-10 rounded-xl w-28" palette="indigo" />
60+
</div>
61+
</div>
62+
63+
{/* Streak card */}
64+
<div className="card-gradient text-white border-none mb-8">
65+
<div className="flex flex-col md:flex-row items-center justify-between gap-6">
66+
<div className="flex items-center gap-6">
67+
<SkeletonBox className="w-20 h-20 rounded-2xl bg-white/20" palette="indigo" />
68+
<div className="space-y-2">
69+
<SkeletonBox className="h-7 rounded w-40 bg-white/20" palette="indigo" />
70+
<SkeletonBox className="h-4 rounded w-64 bg-white/20" palette="indigo" />
71+
</div>
72+
</div>
73+
<div className="flex gap-3">
74+
{[1, 2, 3].map((i) => (
75+
<SkeletonBox
76+
key={i}
77+
className="h-16 w-16 rounded-xl bg-white/20"
78+
palette="indigo"
79+
/>
80+
))}
81+
</div>
82+
</div>
83+
</div>
84+
85+
{/* Donation history card */}
86+
<div className="card">
87+
<SkeletonBox className="h-6 rounded w-40 mb-5" palette="indigo" />
88+
<div className="space-y-2">
89+
{[1, 2, 3].map((i) => (
90+
<div
91+
key={i}
92+
className="flex items-center gap-4 p-4 rounded-xl bg-[rgba(99,102,241,0.04)] dark:bg-[rgba(129,140,248,0.06)]"
93+
>
94+
<SkeletonAvatar size="sm" palette="indigo" />
95+
<div className="flex-1 space-y-2">
96+
<SkeletonBox className="h-3 rounded w-1/3" palette="indigo" />
97+
<SkeletonBox className="h-2 rounded w-1/4" palette="indigo" />
98+
</div>
99+
<SkeletonBadge className="w-20" palette="indigo" />
100+
</div>
101+
))}
102+
</div>
103+
</div>
104+
</div>
105+
);
106+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* components/DonorProfileSkeleton.tsx
3+
*
4+
* Loading placeholder that mirrors the donor profile page:
5+
* - header card (avatar + name + address pill + share button)
6+
* - stats row (Total Donated / Projects / Member Since)
7+
* - earned badges card
8+
* - claim NFT card
9+
* - recent donations list
10+
*
11+
* Uses only the existing primitives in `components/Skeleton.tsx`.
12+
*/
13+
import {
14+
SkeletonBox,
15+
SkeletonAvatar,
16+
SkeletonBadge,
17+
} from "@/components/Skeleton";
18+
19+
export default function DonorProfileSkeleton() {
20+
return (
21+
<div className="min-h-screen bg-leaf">
22+
<div className="max-w-2xl mx-auto px-4 py-10 space-y-6 animate-pulse pointer-events-none">
23+
{/* Header card */}
24+
<div className="card shadow-green">
25+
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
26+
<div className="flex items-center gap-4">
27+
<SkeletonAvatar size="lg" palette="forest" />
28+
<div className="space-y-2">
29+
<SkeletonBox
30+
className="h-5 rounded w-40"
31+
palette="forest"
32+
/>
33+
<SkeletonBox
34+
className="h-3 rounded w-28"
35+
palette="forest"
36+
/>
37+
</div>
38+
</div>
39+
<SkeletonBox className="h-9 rounded-xl w-24" palette="forest" />
40+
</div>
41+
<div className="mt-4 pt-4 border-t border-[rgba(34,114,57,0.08)]">
42+
<SkeletonBox className="h-4 rounded w-full" palette="forest" />
43+
<SkeletonBox
44+
className="h-4 rounded w-2/3 mt-2"
45+
palette="forest"
46+
/>
47+
</div>
48+
</div>
49+
50+
{/* Stats row */}
51+
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
52+
{[0, 1, 2].map((i) => (
53+
<div key={i} className="card h-20" />
54+
))}
55+
</div>
56+
57+
{/* Earned badges */}
58+
<div className="card">
59+
<SkeletonBox className="h-5 rounded w-32 mb-3" palette="forest" />
60+
<div className="flex flex-wrap gap-2">
61+
{[1, 2].map((i) => (
62+
<SkeletonBadge key={i} className="w-24 h-7" palette="forest" />
63+
))}
64+
</div>
65+
</div>
66+
67+
{/* Claim NFT card */}
68+
<div className="card">
69+
<SkeletonBox className="h-5 rounded w-40 mb-2" palette="forest" />
70+
<SkeletonBox className="h-3 rounded w-3/4 mb-4" palette="forest" />
71+
<SkeletonBox className="h-10 rounded-xl w-full" palette="forest" />
72+
</div>
73+
74+
{/* Recent donations */}
75+
<div className="card">
76+
<SkeletonBox className="h-5 rounded w-36 mb-3" palette="forest" />
77+
<div className="space-y-2">
78+
{[1, 2, 3].map((i) => (
79+
<div
80+
key={i}
81+
className="flex items-center gap-4 p-4 rounded-xl bg-[rgba(34,114,57,0.04)] border border-transparent"
82+
>
83+
<SkeletonAvatar size="sm" palette="forest" />
84+
<div className="flex-1 space-y-2">
85+
<SkeletonBox className="h-3 rounded w-1/3" palette="forest" />
86+
<SkeletonBox className="h-2 rounded w-1/4" palette="forest" />
87+
</div>
88+
<SkeletonBox className="h-4 rounded w-16" palette="forest" />
89+
</div>
90+
))}
91+
</div>
92+
</div>
93+
</div>
94+
</div>
95+
);
96+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* components/GovernanceSkeleton.tsx
3+
*
4+
* Loading placeholder that mirrors the governance page:
5+
* - page header + intro
6+
* - wallet / badge status card
7+
* - quorum notice + weight legend
8+
* - proposal cards (title, meta, progress bar, vote buttons)
9+
*
10+
* Uses only the existing primitives in `components/Skeleton.tsx`.
11+
*/
12+
import { SkeletonBox, SkeletonProgressBar } from "@/components/Skeleton";
13+
import PageMeta from "@/components/PageMeta";
14+
15+
export default function GovernanceSkeleton() {
16+
return (
17+
<div className="min-h-screen bg-[#fcfdfc] font-body text-forest-900 pb-20">
18+
<PageMeta
19+
title="Governance | Stellar IndigoPay"
20+
description="Vote on project verification proposals with your impact badge."
21+
canonicalUrl="https://stellar-indigopay.app/governance"
22+
/>
23+
<main className="max-w-3xl mx-auto px-4 py-12 sm:px-6">
24+
{/* Header */}
25+
<div className="mb-10">
26+
<SkeletonBox className="h-10 rounded w-72 mb-3" palette="indigo" />
27+
<SkeletonBox className="h-4 rounded w-2/3" palette="indigo" />
28+
</div>
29+
30+
{/* Wallet / badge status card */}
31+
<div className="card mb-8 rounded-2xl p-5">
32+
<div className="flex items-center justify-between gap-4">
33+
<SkeletonBox className="h-4 rounded w-40" palette="indigo" />
34+
<SkeletonBox className="h-8 rounded-full w-28" palette="indigo" />
35+
</div>
36+
</div>
37+
38+
{/* Quorum notice */}
39+
<div className="mb-6 rounded-xl bg-[rgba(245,158,11,0.06)] border border-[rgba(245,158,11,0.20)] px-4 py-3">
40+
<SkeletonBox className="h-4 rounded w-full" palette="indigo" />
41+
</div>
42+
43+
{/* Weight legend */}
44+
<div className="mb-6 flex flex-wrap gap-3">
45+
{[1, 2, 3, 4].map((i) => (
46+
<SkeletonBox key={i} className="h-4 rounded w-28" palette="indigo" />
47+
))}
48+
</div>
49+
50+
{/* Proposal cards */}
51+
<div className="space-y-4 py-4">
52+
{[1, 2, 3].map((i) => (
53+
<div
54+
key={i}
55+
className="card rounded-2xl p-5 animate-pulse pointer-events-none"
56+
>
57+
<div className="flex items-start justify-between gap-4 mb-4">
58+
<div className="flex-1 space-y-2">
59+
<SkeletonBox className="h-5 rounded w-2/3" palette="indigo" />
60+
<SkeletonBox className="h-3 rounded w-1/3" palette="indigo" />
61+
</div>
62+
<SkeletonBox className="h-6 rounded-full w-16" palette="indigo" />
63+
</div>
64+
<SkeletonProgressBar className="mb-4" palette="indigo" />
65+
<div className="flex gap-2">
66+
<SkeletonBox className="h-10 rounded-xl flex-1" palette="indigo" />
67+
<SkeletonBox className="h-10 rounded-xl flex-1" palette="indigo" />
68+
</div>
69+
</div>
70+
))}
71+
</div>
72+
</main>
73+
</div>
74+
);
75+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/**
2+
* components/ImpactSkeleton.tsx
3+
*
4+
* Full-page loading placeholder that mirrors the global impact dashboard:
5+
* - header
6+
* - 5 stat cards
7+
* - world map card
8+
* - category breakdown card
9+
* - top impact leaders card
10+
* - community CTA
11+
*
12+
* Uses only the existing primitives in `components/Skeleton.tsx`.
13+
*/
14+
import {
15+
SkeletonBox,
16+
SkeletonText,
17+
SkeletonAvatar,
18+
} from "@/components/Skeleton";
19+
20+
export default function ImpactSkeleton() {
21+
return (
22+
<div className="min-h-screen bg-[#FAFAFE] dark:bg-[#0A0A1A] font-body text-[#0F172A] dark:text-[#E2E8F0] pb-20">
23+
<main className="max-w-7xl mx-auto px-4 py-12 sm:px-6 lg:px-8">
24+
{/* Header */}
25+
<div className="text-center mb-16">
26+
<SkeletonBox className="h-12 rounded w-80 mx-auto mb-4" palette="indigo" />
27+
<SkeletonBox className="h-4 rounded w-96 mx-auto" palette="indigo" />
28+
</div>
29+
30+
{/* Global stats grid */}
31+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6 mb-16">
32+
{[1, 2, 3, 4, 5].map((i) => (
33+
<div
34+
key={i}
35+
className="card rounded-3xl p-8 border border-[rgba(99,102,241,0.08)] dark:border-[rgba(129,140,248,0.10)]"
36+
>
37+
<SkeletonBox
38+
className="w-12 h-12 rounded-2xl mb-6"
39+
palette="indigo"
40+
/>
41+
<SkeletonBox
42+
className="h-3 rounded w-1/2 mb-3"
43+
palette="indigo"
44+
/>
45+
<SkeletonBox className="h-8 rounded w-24" palette="indigo" />
46+
</div>
47+
))}
48+
</div>
49+
50+
{/* World map card */}
51+
<div className="card rounded-3xl p-8 mb-16">
52+
<SkeletonBox className="h-7 rounded w-48 mb-6" palette="indigo" />
53+
<SkeletonBox className="h-72 rounded-2xl w-full" palette="indigo" />
54+
</div>
55+
56+
{/* Category breakdown */}
57+
<div className="card rounded-3xl p-8 mb-16">
58+
<SkeletonBox className="h-7 rounded w-56 mb-6" palette="indigo" />
59+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
60+
{[1, 2, 3, 4].map((i) => (
61+
<div
62+
key={i}
63+
className="flex items-center justify-between rounded-2xl border border-[rgba(99,102,241,0.08)] dark:border-[rgba(129,140,248,0.10)] bg-[rgba(99,102,241,0.04)] dark:bg-[rgba(129,140,248,0.06)] p-5"
64+
>
65+
<div className="space-y-2">
66+
<SkeletonBox className="h-4 rounded w-32" palette="indigo" />
67+
<SkeletonBox className="h-3 rounded w-24" palette="indigo" />
68+
</div>
69+
<SkeletonBox className="h-5 rounded w-20" palette="indigo" />
70+
</div>
71+
))}
72+
</div>
73+
</div>
74+
75+
{/* Top impact leaders */}
76+
<div className="card rounded-3xl shadow-indigo p-8 mb-16">
77+
<SkeletonBox className="h-7 rounded w-52 mb-8" palette="indigo" />
78+
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
79+
{[1, 2, 3].map((i) => (
80+
<div
81+
key={i}
82+
className="flex flex-col items-center text-center p-6 bg-[rgba(99,102,241,0.04)] dark:bg-[rgba(129,140,248,0.06)] rounded-2xl"
83+
>
84+
<SkeletonAvatar size="lg" palette="indigo" />
85+
<SkeletonBox className="h-4 rounded w-28 mt-4" palette="indigo" />
86+
<SkeletonBox className="h-3 rounded w-20 mt-2" palette="indigo" />
87+
<SkeletonBox className="h-6 rounded-full w-16 mt-4" palette="indigo" />
88+
</div>
89+
))}
90+
</div>
91+
</div>
92+
93+
{/* Community CTA */}
94+
<div className="text-center py-10">
95+
<SkeletonBox className="h-7 rounded w-64 mx-auto mb-4" palette="indigo" />
96+
<SkeletonText lines={1} widths={["w-1/3"]} className="max-w-xs mx-auto mb-6" palette="indigo" />
97+
<SkeletonBox className="h-12 rounded-xl w-48 mx-auto" palette="indigo" />
98+
</div>
99+
</main>
100+
</div>
101+
);
102+
}

0 commit comments

Comments
 (0)