Skip to content

Commit 76efd36

Browse files
perf(bundle): code-split Activity feed and Admin panel via next/dynamic
- Extract desktop/mobile row renderers into ActivityFeed.tsx and dynamic-import it in activity/page.tsx; removes AddressLink, RelativeTime, Card, eventData helpers, and row-specific icons from the activity route's initial chunk - Extract both management sections and dialogs into AdminPanel.tsx and dynamic-import it in admin/page.tsx; removes all mutation hooks, Select, Dialog, Input, PostUpdateForm, and CampaignStatusBadge from the admin route's initial chunk (panel only loads after ownership is confirmed) - Add dimension-matched skeleton fallbacks for both split points to prevent layout shift during chunk load
1 parent 2f66932 commit 76efd36

4 files changed

Lines changed: 639 additions & 515 deletions

File tree

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
"use client";
2+
3+
import React from "react";
4+
import { Card } from "@/components/ui/card";
5+
import { AddressLink } from "@/components/AddressLink";
6+
import { ErrorBoundary } from "@/components/ErrorBoundary";
7+
import {
8+
MISSING_FIELD,
9+
formatEventAmount,
10+
formatTxHash,
11+
getCampaignId,
12+
getEventField,
13+
} from "@/lib/eventData";
14+
import { RelativeTime } from "@/components/RelativeTime";
15+
import { ArrowUpRight, Megaphone, Trophy } from "lucide-react";
16+
17+
const ZERO_ADDRESS = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF";
18+
19+
function normalizeAddress(value: unknown): string | null {
20+
if (!value) return null;
21+
const str = value.toString();
22+
if (str === ZERO_ADDRESS) return null;
23+
return str.length === 56 && str.startsWith("G") ? str : null;
24+
}
25+
26+
function ledgerLabel(ledger: unknown): string {
27+
return ledger === null || ledger === undefined || ledger === "" ? MISSING_FIELD : String(ledger);
28+
}
29+
30+
// ---------------------------------------------------------------------------
31+
// Shared data-preparation hook — keeps row components thin
32+
// ---------------------------------------------------------------------------
33+
34+
function useActivityData(event: any) {
35+
const id = getCampaignId(event);
36+
const ledger = ledgerLabel(event?.ledger);
37+
const createdAt = event?.createdAt ? new Date(event.createdAt) : null;
38+
const when =
39+
createdAt && !Number.isNaN(createdAt.getTime()) ? (
40+
<RelativeTime date={createdAt} fallback={`Ledger ${ledger}`} />
41+
) : (
42+
`Ledger ${ledger}`
43+
);
44+
45+
let icon = <Megaphone className="w-4 h-4 text-blue-500" />;
46+
let iconBg = "bg-blue-500/10";
47+
let label = event?.topic ?? MISSING_FIELD;
48+
let body: React.ReactNode = null;
49+
50+
if (event?.topic === "received") {
51+
const donor = normalizeAddress(getEventField(event, 1));
52+
icon = <ArrowUpRight className="w-4 h-4 text-green-500" />;
53+
iconBg = "bg-green-500/10";
54+
label = "Donated";
55+
body = (
56+
<>
57+
<span className="font-bold">{formatEventAmount(event, 2)}</span> donated
58+
{donor ? (
59+
<>
60+
{" "}
61+
by <AddressLink address={donor} className="text-muted-foreground" />
62+
</>
63+
) : (
64+
<>
65+
{" "}
66+
by <span className="text-muted-foreground">Anonymous</span>
67+
</>
68+
)}
69+
{id && <> to Campaign #{id}</>}
70+
</>
71+
);
72+
} else if (event?.topic === "created") {
73+
label = "Created";
74+
body = (
75+
<>
76+
New campaign{id && <> #{id}</>} created with a target of{" "}
77+
<span className="font-bold">{formatEventAmount(event, 3)}</span>
78+
</>
79+
);
80+
} else if (event?.topic === "claimed") {
81+
const beneficiary = normalizeAddress(getEventField(event, 1));
82+
icon = <Trophy className="w-4 h-4 text-purple-500" />;
83+
iconBg = "bg-purple-500/10";
84+
label = "Claimed";
85+
body = (
86+
<>
87+
<span className="font-bold">{formatEventAmount(event, 3)}</span> claimed
88+
{beneficiary ? (
89+
<>
90+
{" "}
91+
by <AddressLink address={beneficiary} className="text-muted-foreground" />
92+
</>
93+
) : (
94+
<> by beneficiary</>
95+
)}
96+
{id && <> from Campaign #{id}</>}
97+
</>
98+
);
99+
} else {
100+
body = <span className="text-muted-foreground">{label}</span>;
101+
}
102+
103+
return {
104+
id,
105+
when,
106+
ledger,
107+
icon,
108+
iconBg,
109+
label,
110+
body,
111+
txHash: event?.txHash,
112+
txLabel: formatTxHash(event?.txHash),
113+
};
114+
}
115+
116+
// ---------------------------------------------------------------------------
117+
// Row renderers
118+
// ---------------------------------------------------------------------------
119+
120+
function ActivityRowDesktop({ event }: { event: any }) {
121+
const { icon, iconBg, label, body, when, ledger, txHash, txLabel } = useActivityData(event);
122+
123+
return (
124+
<tr className="hover:bg-muted/10 transition-colors">
125+
<td className="px-4 py-3">
126+
<div className="flex items-center gap-2">
127+
<div className={`p-1.5 rounded-full shrink-0 ${iconBg}`}>{icon}</div>
128+
<span className="uppercase text-[10px] font-bold tracking-wider text-muted-foreground">
129+
{label}
130+
</span>
131+
</div>
132+
</td>
133+
<td className="px-4 py-3 text-sm">{body}</td>
134+
<td className="px-4 py-3 text-xs text-muted-foreground">
135+
{when} <span className="hidden lg:inline-block"> • Ledger {ledger}</span>
136+
</td>
137+
<td className="px-4 py-3 text-right">
138+
{txLabel ? (
139+
<a
140+
href={`https://stellar.expert/explorer/testnet/tx/${txHash}`}
141+
target="_blank"
142+
rel="noreferrer"
143+
className="font-mono text-xs text-primary hover:underline"
144+
>
145+
{txLabel}
146+
</a>
147+
) : (
148+
<span className="text-muted-foreground text-xs">N/A</span>
149+
)}
150+
</td>
151+
</tr>
152+
);
153+
}
154+
155+
function ActivityRowMobile({ event }: { event: any }) {
156+
const { icon, iconBg, label, body, when, txHash, txLabel } = useActivityData(event);
157+
158+
return (
159+
<div className="flex flex-col gap-3 p-4 border rounded-lg bg-card">
160+
<div className="flex justify-between items-center">
161+
<div className="flex items-center gap-2">
162+
<div className={`p-1.5 rounded-full shrink-0 ${iconBg}`}>{icon}</div>
163+
<span className="uppercase text-[10px] font-bold tracking-wider text-muted-foreground">
164+
{label}
165+
</span>
166+
</div>
167+
<span className="text-xs text-muted-foreground">{when}</span>
168+
</div>
169+
<p className="text-sm">{body}</p>
170+
<div className="pt-3 border-t flex justify-between items-center text-xs">
171+
<span className="text-muted-foreground">Tx Hash</span>
172+
{txLabel ? (
173+
<a
174+
href={`https://stellar.expert/explorer/testnet/tx/${txHash}`}
175+
target="_blank"
176+
rel="noreferrer"
177+
className="font-mono text-primary hover:underline"
178+
>
179+
{txLabel}
180+
</a>
181+
) : (
182+
<span className="text-muted-foreground">N/A</span>
183+
)}
184+
</div>
185+
</div>
186+
);
187+
}
188+
189+
// ---------------------------------------------------------------------------
190+
// ActivityFeed — the exported split point
191+
// ---------------------------------------------------------------------------
192+
193+
interface ActivityFeedProps {
194+
visible: any[];
195+
showIndicator: boolean;
196+
}
197+
198+
export function ActivityFeed({ visible, showIndicator }: ActivityFeedProps) {
199+
return (
200+
<ErrorBoundary heading="Activity feed">
201+
{/* Desktop table */}
202+
<div className="relative hidden md:block overflow-x-auto">
203+
{showIndicator && (
204+
<div className="absolute -top-12 left-1/2 -translate-x-1/2 z-10 bg-primary text-primary-foreground px-4 py-1.5 rounded-full text-sm font-medium shadow-md animate-in fade-in slide-in-from-top-4 duration-300">
205+
New activity
206+
</div>
207+
)}
208+
<Card>
209+
<table className="w-full text-sm text-left">
210+
<thead className="text-xs text-muted-foreground uppercase bg-muted/20">
211+
<tr>
212+
<th className="px-4 py-3 rounded-tl-md">Status</th>
213+
<th className="px-4 py-3">Action</th>
214+
<th className="px-4 py-3">Time</th>
215+
<th className="px-4 py-3 rounded-tr-md text-right">Tx Hash</th>
216+
</tr>
217+
</thead>
218+
<tbody className="divide-y divide-border">
219+
{visible.map((event: any, idx: number) => (
220+
<ActivityRowDesktop key={event?.id ?? `event-${idx}`} event={event} />
221+
))}
222+
</tbody>
223+
</table>
224+
</Card>
225+
</div>
226+
227+
{/* Mobile cards */}
228+
<div className="relative md:hidden space-y-4">
229+
{showIndicator && (
230+
<div className="absolute -top-12 left-1/2 -translate-x-1/2 z-10 bg-primary text-primary-foreground px-4 py-1.5 rounded-full text-sm font-medium shadow-md animate-in fade-in slide-in-from-top-4 duration-300">
231+
New activity
232+
</div>
233+
)}
234+
{visible.map((event: any, idx: number) => (
235+
<ActivityRowMobile key={event?.id ?? `event-${idx}`} event={event} />
236+
))}
237+
</div>
238+
</ErrorBoundary>
239+
);
240+
}

0 commit comments

Comments
 (0)