Skip to content

Commit d7541e0

Browse files
authored
Merge pull request #50 from khalifa-zoro/Added-loading-skeletons-1
Closes #10, Added loading skeletons 1
2 parents aa6d515 + 3192bc9 commit d7541e0

9 files changed

Lines changed: 237 additions & 63 deletions

File tree

backend/package-lock.json

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package-lock.json

Lines changed: 2 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/src/App.tsx

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ function App() {
6565
const [campaigns, setCampaigns] = useState<Campaign[]>([]);
6666
const [issues, setIssues] = useState<OpenIssue[]>([]);
6767
const [history, setHistory] = useState<CampaignEvent[]>([]);
68+
const [isCampaignsLoading, setIsCampaignsLoading] = useState(false);
69+
const [isSelectedLoading, setIsSelectedLoading] = useState(false);
70+
const [initialLoad, setInitialLoad] = useState(true);
6871
const [selectedCampaignId, setSelectedCampaignId] = useState<string | null>(null);
6972
const [selectedCampaignDetails, setSelectedCampaignDetails] = useState<Campaign | null>(null);
7073
const [createError, setCreateError] = useState<string | null>(null);
@@ -73,13 +76,22 @@ function App() {
7376
const [pendingPledgeCampaignId, setPendingPledgeCampaignId] = useState<string | null>(null);
7477

7578
async function refreshCampaigns(nextSelectedId?: string | null) {
76-
const data = await listCampaigns();
77-
setCampaigns(data);
78-
79-
const candidateId =
80-
nextSelectedId ?? selectedCampaignId ?? (data.length > 0 ? data[0].id : null);
81-
const exists = data.some((campaign) => campaign.id === candidateId);
82-
setSelectedCampaignId(exists ? candidateId : data[0]?.id ?? null);
79+
const startedAt = Date.now();
80+
setIsCampaignsLoading(true);
81+
try {
82+
const data = await listCampaigns();
83+
setCampaigns(data);
84+
85+
const candidateId =
86+
nextSelectedId ?? selectedCampaignId ?? (data.length > 0 ? data[0].id : null);
87+
const exists = data.some((campaign) => campaign.id === candidateId);
88+
setSelectedCampaignId(exists ? candidateId : data[0]?.id ?? null);
89+
} finally {
90+
const elapsed = Date.now() - startedAt;
91+
const minMs = 300;
92+
if (elapsed < minMs) await delay(minMs - elapsed);
93+
setIsCampaignsLoading(false);
94+
}
8395
}
8496

8597
async function refreshHistory(campaignId: string | null) {
@@ -88,29 +100,57 @@ function App() {
88100
return;
89101
}
90102

91-
const data = await getCampaignHistory(campaignId);
92-
setHistory(data);
103+
const startedAt = Date.now();
104+
setIsSelectedLoading(true);
105+
try {
106+
const data = await getCampaignHistory(campaignId);
107+
setHistory(data);
108+
} finally {
109+
const elapsed = Date.now() - startedAt;
110+
const minMs = 200;
111+
if (elapsed < minMs) await delay(minMs - elapsed);
112+
setIsSelectedLoading(false);
113+
}
93114
}
94115

95116
async function refreshSelectedCampaign(campaignId: string | null) {
96117
if (!campaignId) {
97118
setSelectedCampaignDetails(null);
98119
return;
99120
}
100-
const campaign = await getCampaign(campaignId);
101-
setSelectedCampaignDetails(campaign);
121+
const startedAt = Date.now();
122+
setIsSelectedLoading(true);
123+
try {
124+
const campaign = await getCampaign(campaignId);
125+
setSelectedCampaignDetails(campaign);
126+
} finally {
127+
const elapsed = Date.now() - startedAt;
128+
const minMs = 200;
129+
if (elapsed < minMs) await delay(minMs - elapsed);
130+
setIsSelectedLoading(false);
131+
}
102132
}
103133

104134
useEffect(() => {
105135
async function bootstrap() {
106-
const [campaignData, issueData] = await Promise.all([
107-
listCampaigns(),
108-
listOpenIssues(),
109-
]);
110-
111-
setCampaigns(campaignData);
112-
setIssues(issueData);
113-
setSelectedCampaignId(campaignData[0]?.id ?? null);
136+
const startedAt = Date.now();
137+
setIsCampaignsLoading(true);
138+
try {
139+
const [campaignData, issueData] = await Promise.all([
140+
listCampaigns(),
141+
listOpenIssues(),
142+
]);
143+
144+
setCampaigns(campaignData);
145+
setIssues(issueData);
146+
setSelectedCampaignId(campaignData[0]?.id ?? null);
147+
} finally {
148+
const elapsed = Date.now() - startedAt;
149+
const minMs = 350;
150+
if (elapsed < minMs) await delay(minMs - elapsed);
151+
setIsCampaignsLoading(false);
152+
setInitialLoad(false);
153+
}
114154
}
115155

116156
void bootstrap();
@@ -304,6 +344,7 @@ function App() {
304344
actionError={actionError}
305345
actionMessage={actionMessage}
306346
isPledgePending={pendingPledgeCampaignId === selectedCampaignId}
347+
isLoading={isSelectedLoading || initialLoad}
307348
onPledge={handlePledge}
308349
onClaim={handleClaim}
309350
onRefund={handleRefund}
@@ -312,13 +353,14 @@ function App() {
312353

313354
<CampaignsTable
314355
campaigns={campaigns}
356+
isLoading={isCampaignsLoading || initialLoad}
315357
selectedCampaignId={selectedCampaignId}
316358
onSelect={setSelectedCampaignId}
317359
/>
318360

319361
<section className="secondary-grid">
320-
<CampaignTimeline history={history} />
321-
<IssueBacklog issues={issues} />
362+
<CampaignTimeline history={history} isLoading={isSelectedLoading || initialLoad} />
363+
<IssueBacklog issues={issues} isLoading={isSelectedLoading || initialLoad} />
322364
</section>
323365
</div>
324366
);

frontend/src/components/CampaignDetailPanel.tsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ContributorSummary } from "./ContributorSummary";
44

55
interface CampaignDetailPanelProps {
66
campaign: Campaign | null;
7+
isLoading?: boolean;
78
actionError?: string | null;
89
actionMessage?: string | null;
910
isPledgePending?: boolean;
@@ -14,6 +15,7 @@ interface CampaignDetailPanelProps {
1415

1516
export function CampaignDetailPanel({
1617
campaign,
18+
isLoading,
1719
actionError,
1820
actionMessage,
1921
isPledgePending = false,
@@ -30,6 +32,32 @@ export function CampaignDetailPanel({
3032
setAmount("25");
3133
}, [campaign?.id]);
3234

35+
if (isLoading) {
36+
return (
37+
<section className="card detail-panel">
38+
<div className="section-heading">
39+
<h2>
40+
<div className="skeleton skeleton-line" style={{ width: 220 }} />
41+
</h2>
42+
<p className="muted">
43+
<div className="skeleton skeleton-line" style={{ width: 320, height: 14 }} />
44+
</p>
45+
</div>
46+
47+
<div className="detail-grid">
48+
{Array.from({ length: 4 }).map((_, i) => (
49+
<article key={i} className="detail-stat">
50+
<div className="skeleton skeleton-line" style={{ width: 120 }} />
51+
<div className="skeleton skeleton-line" style={{ width: 80, height: 18, marginTop: 8 }} />
52+
</article>
53+
))}
54+
</div>
55+
56+
<div className="skeleton" style={{ height: 120, borderRadius: 12 }} />
57+
</section>
58+
);
59+
}
60+
3361
if (!campaign) {
3462
return <section className="card empty-state">Pick a campaign from the board to manage it.</section>;
3563
}
@@ -91,7 +119,7 @@ export function CampaignDetailPanel({
91119
</article>
92120
</div>
93121

94-
<ContributorSummary pledges={activeCampaign.pledges} assetCode={activeCampaign.assetCode} />
122+
<ContributorSummary pledges={activeCampaign.pledges} assetCode={activeCampaign.assetCode} isLoading={isLoading} />
95123

96124
<form className="form-grid" onSubmit={handlePledge}>
97125
<label className="field-group">

frontend/src/components/CampaignTimeline.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { CampaignEvent } from "../types/campaign";
22

33
interface CampaignTimelineProps {
44
history: CampaignEvent[];
5+
isLoading?: boolean;
56
}
67

78
function formatTimestamp(unixSeconds: number): string {
@@ -23,15 +24,27 @@ function describeEvent(event: CampaignEvent): string {
2324
}
2425
}
2526

26-
export function CampaignTimeline({ history }: CampaignTimelineProps) {
27+
export function CampaignTimeline({ history, isLoading }: CampaignTimelineProps) {
2728
return (
2829
<section className="card">
2930
<div className="section-heading">
3031
<h2>Timeline</h2>
3132
<p className="muted">Each action is stored locally so contributors can follow campaign activity.</p>
3233
</div>
3334

34-
{history.length === 0 ? (
35+
{isLoading ? (
36+
<div className="timeline">
37+
{Array.from({ length: 4 }).map((_, idx) => (
38+
<article key={idx} className="timeline-item">
39+
<div className="timeline-dot" aria-hidden />
40+
<div className="timeline-copy">
41+
<div className="skeleton skeleton-line" style={{ width: 160 }} />
42+
<div className="skeleton skeleton-line" style={{ width: 100, height: 12 }} />
43+
</div>
44+
</article>
45+
))}
46+
</div>
47+
) : history.length === 0 ? (
3548
<div className="empty-state">Select a campaign to see lifecycle events.</div>
3649
) : (
3750
<div className="timeline">

0 commit comments

Comments
 (0)