Skip to content

Commit 5e78dab

Browse files
authored
feat: skeleton (#885)
## Description - adding common skeleton components - adding skeleton to homepage ## Type of change - [x] Bug fix ## Screenshots https://github.com/user-attachments/assets/c7133d38-82cc-47a8-a4d7-9d3a592ad0f4 ## Testing 1. open [homepage](http://localhost:3000/) and see loading state see [storybook](http://localhost:6006/?path=/docs/components-shared-ui-skeletons--docs) ## Related Issues [HOO-265](https://linear.app/solana-fndn/issue/HOO-265/add-skeleton-loaders-to-explorer-pages) [HOO-347](https://linear.app/solana-fndn/issue/HOO-347/build-skeleton-components-for-various-layouts) [HOO-348](https://linear.app/solana-fndn/issue/HOO-348/wrap-main-page-components-that-require-live-data-with-skeletons) ## Checklist - [x] I have added tests that prove my fix/feature works - [x] All tests pass locally and in CI - [x] I have run `build:info` script to update build information - [x] CI/CD checks pass - [x] I have included screenshots for protocol screens (if applicable)
1 parent a22de84 commit 5e78dab

File tree

5 files changed

+321
-25
lines changed

5 files changed

+321
-25
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import type { Meta, StoryObj } from '@storybook/react';
2+
import { expect } from 'storybook/test';
3+
4+
import {
5+
ImageSliderSkeleton,
6+
RichListSkeleton,
7+
RichRowSkeleton,
8+
SimpleCardSkeleton,
9+
StatsTableSkeleton,
10+
TableCardSkeleton,
11+
TableRowSkeleton,
12+
} from './Skeletons';
13+
14+
const hasPulse = (canvasElement: HTMLElement) =>
15+
expect(canvasElement.querySelectorAll('.e-animate-pulse').length).toBeGreaterThan(0);
16+
17+
function TableWrapper({ children }: { children: React.ReactNode }) {
18+
return (
19+
<div className="table-responsive e-mb-0">
20+
<table className="table table-sm card-table table-nowrap">
21+
<tbody className="list">{children}</tbody>
22+
</table>
23+
</div>
24+
);
25+
}
26+
27+
const meta = {
28+
component: TableRowSkeleton,
29+
tags: ['autodocs', 'test'],
30+
title: 'Components/Shared/UI/Skeletons',
31+
} satisfies Meta<typeof TableRowSkeleton>;
32+
33+
export default meta;
34+
type Story = StoryObj<typeof meta>;
35+
36+
export const TableRow: Story = {
37+
play: async ({ canvasElement }) => {
38+
await hasPulse(canvasElement);
39+
},
40+
render: args => (
41+
<TableWrapper>
42+
<TableRowSkeleton {...args} />
43+
<TableRowSkeleton {...args} />
44+
<TableRowSkeleton {...args} />
45+
</TableWrapper>
46+
),
47+
};
48+
49+
export const RichRow: StoryObj<typeof RichRowSkeleton> = {
50+
play: async ({ canvasElement }) => {
51+
await hasPulse(canvasElement);
52+
},
53+
render: () => (
54+
<TableWrapper>
55+
<RichRowSkeleton />
56+
<RichRowSkeleton />
57+
<RichRowSkeleton />
58+
</TableWrapper>
59+
),
60+
};
61+
62+
export const SimpleCard: StoryObj<typeof SimpleCardSkeleton> = {
63+
play: async ({ canvasElement }) => {
64+
await hasPulse(canvasElement);
65+
},
66+
render: () => <SimpleCardSkeleton />,
67+
};
68+
69+
export const SimpleCardWithTitle: StoryObj<typeof SimpleCardSkeleton> = {
70+
name: 'SimpleCard / with title',
71+
play: async ({ canvasElement }) => {
72+
await hasPulse(canvasElement);
73+
},
74+
render: () => <SimpleCardSkeleton withTitle />,
75+
};
76+
77+
export const RichList: StoryObj<typeof RichListSkeleton> = {
78+
play: async ({ canvasElement }) => {
79+
await hasPulse(canvasElement);
80+
},
81+
render: () => <RichListSkeleton />,
82+
};
83+
84+
export const StatsTable: StoryObj<typeof StatsTableSkeleton> = {
85+
play: async ({ canvasElement }) => {
86+
await hasPulse(canvasElement);
87+
},
88+
render: () => <StatsTableSkeleton />,
89+
};
90+
91+
export const TableCardCustom: StoryObj<typeof TableCardSkeleton> = {
92+
name: 'TableCard',
93+
play: async ({ canvasElement }) => {
94+
await hasPulse(canvasElement);
95+
},
96+
render: () => <TableCardSkeleton />,
97+
};
98+
99+
export const ImageSlider: StoryObj<typeof ImageSliderSkeleton> = {
100+
play: async ({ canvasElement }) => {
101+
await hasPulse(canvasElement);
102+
},
103+
render: () => <ImageSliderSkeleton />,
104+
};
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { ReactElement } from 'react';
2+
import { Skeleton } from './ui/skeleton';
3+
4+
type ColDef = {
5+
skeleton: string;
6+
td?: string;
7+
};
8+
9+
export function TableRowSkeleton({
10+
cols = [{ skeleton: 'e-h-4 e-w-36', td: 'e-w-full' }, { skeleton: 'e-h-4 e-w-20' }],
11+
}: {
12+
cols?: ColDef[];
13+
}) {
14+
return (
15+
<tr>
16+
{cols.map(({ skeleton, td }, i) => (
17+
<td key={i} className={td}>
18+
<Skeleton className={skeleton} />
19+
</td>
20+
))}
21+
</tr>
22+
);
23+
}
24+
25+
export function RichRowSkeleton() {
26+
return (
27+
<tr>
28+
<td>
29+
<Skeleton className="e-mb-2 e-h-3 e-w-3/5" />
30+
<Skeleton className="e-mb-2 e-h-3 e-w-full" />
31+
<Skeleton className="e-h-3 e-w-2/5" />
32+
</td>
33+
</tr>
34+
);
35+
}
36+
37+
export function SimpleCardSkeleton({ withTitle = false, title }: { withTitle?: boolean; title?: ReactElement }) {
38+
return (
39+
<div className="card e-w-full">
40+
{withTitle && (
41+
<div className="card-header">
42+
<div className="row e-items-center">
43+
<div className="col">
44+
<Skeleton className="e-h-5 e-w-40" />
45+
</div>
46+
</div>
47+
</div>
48+
)}
49+
<div className="card-body">
50+
{title || <Skeleton className="e-h-5 e-w-3/5" />}
51+
<Skeleton className="e-mb-2.5 e-mt-2 e-h-7 e-w-full" />
52+
<Skeleton className="e-h-3 e-w-2/5" />
53+
</div>
54+
</div>
55+
);
56+
}
57+
58+
export function RichListSkeleton({ rows = 2 }: { rows?: number }) {
59+
return (
60+
<div className="card e-flex-1">
61+
<div className="card-header">
62+
<div className="row align-items-center">
63+
<div className="col">
64+
<Skeleton className="e-h-5 e-w-40" />
65+
</div>
66+
</div>
67+
</div>
68+
<div className="table-responsive e-mb-0">
69+
<table className="table table-sm card-table table-nowrap">
70+
<tbody className="list">
71+
{Array.from({ length: rows }).map((_, i) => (
72+
<RichRowSkeleton key={i} />
73+
))}
74+
</tbody>
75+
</table>
76+
</div>
77+
</div>
78+
);
79+
}
80+
81+
export function StatsTableSkeleton({ rows = 8 }: { rows?: number }) {
82+
return (
83+
<div className="card e-flex-1">
84+
<div className="card-header">
85+
<div className="row align-items-center">
86+
<div className="col">
87+
<Skeleton className="e-h-5 e-w-40" />
88+
</div>
89+
</div>
90+
</div>
91+
<div className="table-responsive e-mb-0">
92+
<table className="table table-sm card-table table-nowrap">
93+
<tbody className="list">
94+
{Array.from({ length: rows }).map((_, i) => (
95+
<TableRowSkeleton key={i} />
96+
))}
97+
</tbody>
98+
</table>
99+
</div>
100+
</div>
101+
);
102+
}
103+
104+
export function TableCardSkeleton({ cols = 4, rows = 6 }: { cols?: number; rows?: number }) {
105+
return (
106+
<div className="card">
107+
<div className="card-header">
108+
<Skeleton className="e-h-5 e-w-64" />
109+
</div>
110+
<div className="table-responsive small-headers">
111+
<table className="table">
112+
<thead>
113+
<tr>
114+
{Array.from({ length: cols }).map((_, i) => (
115+
<th key={i}>
116+
<Skeleton className="e-h-4 e-w-1/2" />
117+
</th>
118+
))}
119+
</tr>
120+
</thead>
121+
<tbody>
122+
{Array.from({ length: rows }).map((_, i) => (
123+
<tr key={i}>
124+
{Array.from({ length: cols }).map((_, j) => (
125+
<td key={j}>
126+
<Skeleton className="e-h-4 e-w-1/3" />
127+
</td>
128+
))}
129+
</tr>
130+
))}
131+
</tbody>
132+
</table>
133+
</div>
134+
</div>
135+
);
136+
}
137+
138+
function ImageSliderCardSkeleton() {
139+
return (
140+
<div className="e-h-[200px] e-w-[250px] e-min-w-[250px]">
141+
<Skeleton className="e-mb-3 e-h-[120px] e-w-full" />
142+
<Skeleton className="e-mb-1 e-h-4 e-w-4/5" />
143+
<Skeleton className="e-mb-1 e-h-3 e-w-full" />
144+
<Skeleton className="e-h-3 e-w-3/4" />
145+
</div>
146+
);
147+
}
148+
149+
export function ImageSliderSkeleton() {
150+
return (
151+
<div className="card">
152+
<div className="card-body">
153+
<div className="e-mb-3 e-flex e-justify-between e-border-b e-border-gray-300 e-pb-2">
154+
<Skeleton className="e-h-5 e-w-80" />
155+
<Skeleton className="e-h-4 e-w-40" />
156+
</div>
157+
<div className="e-flex e-gap-4 e-overflow-auto e-pb-3">
158+
<ImageSliderCardSkeleton />
159+
<ImageSliderCardSkeleton />
160+
<ImageSliderCardSkeleton />
161+
<ImageSliderCardSkeleton />
162+
</div>
163+
</div>
164+
</div>
165+
);
166+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { cn } from '../utils';
2+
3+
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
4+
return (
5+
<div
6+
className={cn('e-animate-pulse e-rounded e-bg-heavy-metal-700 motion-reduce:e-animate-none', className)}
7+
{...props}
8+
/>
9+
);
10+
}
11+
12+
export { Skeleton };

app/page.tsx

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

33
import { Epoch } from '@components/common/Epoch';
44
import { ErrorCard } from '@components/common/ErrorCard';
5-
import { LoadingCard } from '@components/common/LoadingCard';
65
import { Slot } from '@components/common/Slot';
76
import { TableCardBody } from '@components/common/TableCardBody';
87
import { TimestampToggle } from '@components/common/TimestampToggle';
@@ -25,6 +24,7 @@ import React from 'react';
2524

2625
import { DeveloperResources } from './components/DeveloperResources';
2726
import { UpcomingFeatures } from './utils/feature-gate/UpcomingFeatures';
27+
import { SimpleCardSkeleton } from './components/shared/Skeletons';
2828

2929
export default function Page() {
3030
return (
@@ -51,6 +51,15 @@ export default function Page() {
5151
);
5252
}
5353

54+
const LoadingStatsCard = ({ title }: { title: string }) => {
55+
return (
56+
<div className="e-flex e-items-center e-gap-2">
57+
<span className="spinner-grow spinner-grow-sm" />
58+
{title}
59+
</div>
60+
);
61+
};
62+
5463
function StakingComponent() {
5564
const { status } = useCluster();
5665
const supply = useSupply();
@@ -89,7 +98,12 @@ function StakingComponent() {
8998
}
9099

91100
if (supply === Status.Idle || supply === Status.Connecting) {
92-
return <LoadingCard message="Loading supply data" />;
101+
return (
102+
<div className="e-flex e-gap-6">
103+
<SimpleCardSkeleton title={<LoadingStatsCard title="Loading supply data" />} />
104+
<SimpleCardSkeleton title={<LoadingStatsCard title="Loading staking data" />} />
105+
</div>
106+
);
93107
} else if (typeof supply === 'string') {
94108
return <ErrorCard text={supply} retry={fetchData} />;
95109
}

bench/BUILD.md

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,31 @@
11
| Type | Route | Size | First Load JS |
22
|------|-------|------|---------------|
3-
| Static | `/` | 17.8 kB | 1.03 MB |
3+
| Static | `/` | 14.7 kB | 1.03 MB |
44
| Static | `/_not-found` | 326 B | 164 kB |
5-
| Dynamic | `/address/[address]` | 13.4 kB | 975 kB |
6-
| Dynamic | `/address/[address]/anchor-account` | 7.47 kB | 1.01 MB |
7-
| Dynamic | `/address/[address]/anchor-program` | 336 B | 892 kB |
8-
| Dynamic | `/address/[address]/attestation` | 6.28 kB | 982 kB |
9-
| Dynamic | `/address/[address]/attributes` | 2.96 kB | 936 kB |
5+
| Dynamic | `/address/[address]` | 12.3 kB | 946 kB |
6+
| Dynamic | `/address/[address]/anchor-account` | 6.29 kB | 1.01 MB |
7+
| Dynamic | `/address/[address]/anchor-program` | 335 B | 892 kB |
8+
| Dynamic | `/address/[address]/attestation` | 7.12 kB | 982 kB |
9+
| Dynamic | `/address/[address]/attributes` | 2.96 kB | 937 kB |
1010
| Dynamic | `/address/[address]/blockhashes` | 2.35 kB | 936 kB |
11-
| Dynamic | `/address/[address]/compression` | 5.6 kB | 972 kB |
12-
| Dynamic | `/address/[address]/concurrent-merkle-tree` | 4.06 kB | 970 kB |
11+
| Dynamic | `/address/[address]/compression` | 5.15 kB | 972 kB |
12+
| Dynamic | `/address/[address]/concurrent-merkle-tree` | 3.61 kB | 970 kB |
1313
| Dynamic | `/address/[address]/domains` | 2.94 kB | 939 kB |
1414
| Dynamic | `/address/[address]/entries` | 3.57 kB | 957 kB |
15-
| Dynamic | `/address/[address]/feature-gate` | 335 B | 892 kB |
16-
| Dynamic | `/address/[address]/idl` | 129 kB | 1.2 MB |
15+
| Dynamic | `/address/[address]/feature-gate` | 334 B | 892 kB |
16+
| Dynamic | `/address/[address]/idl` | 127 kB | 1.2 MB |
1717
| Dynamic | `/address/[address]/instructions` | 1.59 kB | 1.05 MB |
18-
| Dynamic | `/address/[address]/metadata` | 7.55 kB | 951 kB |
19-
| Dynamic | `/address/[address]/nftoken-collection-nfts` | 9.28 kB | 1.02 MB |
20-
| Dynamic | `/address/[address]/program-multisig` | 4.81 kB | 1.01 MB |
18+
| Dynamic | `/address/[address]/metadata` | 9.86 kB | 951 kB |
19+
| Dynamic | `/address/[address]/nftoken-collection-nfts` | 6.66 kB | 985 kB |
20+
| Dynamic | `/address/[address]/program-multisig` | 3.7 kB | 1.01 MB |
2121
| Dynamic | `/address/[address]/rewards` | 4.32 kB | 940 kB |
22-
| Dynamic | `/address/[address]/security` | 9.94 kB | 1.02 MB |
22+
| Dynamic | `/address/[address]/security` | 8.59 kB | 1.03 MB |
2323
| Dynamic | `/address/[address]/slot-hashes` | 4.21 kB | 940 kB |
24-
| Dynamic | `/address/[address]/stake-history` | 4.34 kB | 940 kB |
25-
| Dynamic | `/address/[address]/token-extensions` | 12.9 kB | 1.02 MB |
26-
| Dynamic | `/address/[address]/tokens` | 27.5 kB | 1.13 MB |
27-
| Dynamic | `/address/[address]/transfers` | 3.58 kB | 1.07 MB |
28-
| Dynamic | `/address/[address]/verified-build` | 7.45 kB | 1.01 MB |
24+
| Dynamic | `/address/[address]/stake-history` | 4.33 kB | 940 kB |
25+
| Dynamic | `/address/[address]/token-extensions` | 11.4 kB | 995 kB |
26+
| Dynamic | `/address/[address]/tokens` | 13.7 kB | 1.13 MB |
27+
| Dynamic | `/address/[address]/transfers` | 2.93 kB | 1.07 MB |
28+
| Dynamic | `/address/[address]/verified-build` | 6.29 kB | 1.01 MB |
2929
| Dynamic | `/address/[address]/vote-history` | 4.24 kB | 940 kB |
3030
| Dynamic | `/api/anchor` | 0 B | 0 B |
3131
| Dynamic | `/api/ans-domains/[address]` | 0 B | 0 B |
@@ -53,7 +53,7 @@
5353
| Static | `/opengraph-image.png` | 0 B | 0 B |
5454
| Static | `/supply` | 6.05 kB | 947 kB |
5555
| Static | `/tos` | 325 B | 164 kB |
56-
| Dynamic | `/tx/[signature]` | 53.9 kB | 1.44 MB |
57-
| Dynamic | `/tx/[signature]/inspect` | 631 B | 1.23 MB |
58-
| Static | `/tx/inspector` | 636 B | 1.23 MB |
59-
| Static | `/verified-programs` | 6.34 kB | 173 kB |
56+
| Dynamic | `/tx/[signature]` | 48.5 kB | 1.43 MB |
57+
| Dynamic | `/tx/[signature]/inspect` | 624 B | 1.21 MB |
58+
| Static | `/tx/inspector` | 629 B | 1.21 MB |
59+
| Static | `/verified-programs` | 6.42 kB | 173 kB |

0 commit comments

Comments
 (0)