Skip to content

Commit ea25c11

Browse files
author
waterWang
committed
feat: improve mobile responsiveness of admin dashboard tables (Closes #235)
1 parent 234d56b commit ea25c11

4 files changed

Lines changed: 115 additions & 18 deletions

File tree

app/[communitySlug]/admin/page.tsx

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useParams } from 'next/navigation';
88
import { config } from '@/lib/config';
99
import { isApiError } from '@/lib/api/errors';
1010
import { queryKeys } from '@/lib/query';
11+
import { ResponsiveTable } from '@/components/ui/responsive-table';
1112
import { EmptyState, ErrorState, LoadingState, safeErrorMessage } from '@/components/ui/api-states';
1213
import { AddressText } from '@/components/wallet/address-text';
1314
import { AdminGuard } from '@/components/admin-guard';
@@ -284,7 +285,7 @@ function WebhookLogsContent() {
284285
/>
285286
) : (
286287
<div className="border border-border rounded-lg overflow-hidden bg-card">
287-
<div className="overflow-x-auto">
288+
<ResponsiveTable>
288289
<table className="min-w-full divide-y divide-border text-left text-sm">
289290
<thead className="bg-muted text-muted-foreground uppercase text-xs font-semibold tracking-wider">
290291
<tr>
@@ -304,17 +305,17 @@ function WebhookLogsContent() {
304305
className="hover:bg-muted/50 transition-colors cursor-pointer"
305306
onClick={() => toggleExpand(evt.id)}
306307
>
307-
<td className="px-6 py-4 whitespace-nowrap text-muted-foreground">
308+
<td data-label="" className="px-6 py-4 whitespace-nowrap text-muted-foreground">
308309
<span className="inline-block transition-transform duration-200"
309310
style={{ transform: expandedId === evt.id ? 'rotate(90deg)' : 'rotate(0deg)' }}
310311
>
311312
312313
</span>
313314
</td>
314-
<td className="px-6 py-4 whitespace-nowrap text-muted-foreground font-mono text-xs">
315+
<td data-label="Timestamp" className="px-6 py-4 whitespace-nowrap text-muted-foreground font-mono text-xs">
315316
{new Date(evt.timestamp).toLocaleString()}
316317
</td>
317-
<td className="px-6 py-4 font-semibold font-mono text-xs text-foreground">
318+
<td data-label="Event Type" className="px-6 py-4 font-semibold font-mono text-xs text-foreground">
318319
<div className="flex items-center gap-2">
319320
<span>{evt.eventType}</span>
320321
{evt.isReplay && (
@@ -324,15 +325,15 @@ function WebhookLogsContent() {
324325
)}
325326
</div>
326327
</td>
327-
<td className="px-6 py-4 text-muted-foreground font-mono text-xs">
328+
<td data-label="Target" className="px-6 py-4 text-muted-foreground font-mono text-xs">
328329
<AddressText
329330
address={evt.affectedIdentifier}
330331
label="Target address or resource"
331332
announceInvalid={false}
332333
className="text-muted-foreground"
333334
/>
334335
</td>
335-
<td className="px-6 py-4 whitespace-nowrap">
336+
<td data-label="Status" className="px-6 py-4 whitespace-nowrap">
336337
<span
337338
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold tracking-wide uppercase ${
338339
evt.status === 'success'
@@ -345,11 +346,11 @@ function WebhookLogsContent() {
345346
{evt.status}
346347
</span>
347348
</td>
348-
<td className="px-6 py-4 text-muted-foreground text-xs max-w-xs truncate font-mono">
349+
<td data-label="Payload" className="px-6 py-4 text-muted-foreground text-xs max-w-xs truncate font-mono">
349350
{JSON.stringify(evt.payloadSummary)}
350351
</td>
351352
{isMockMode && (
352-
<td className="px-6 py-4 whitespace-nowrap">
353+
<td data-label="Actions" className="px-6 py-4 whitespace-nowrap">
353354
<Button
354355
size="sm"
355356
variant="outline"
@@ -376,7 +377,7 @@ function WebhookLogsContent() {
376377
))}
377378
</tbody>
378379
</table>
379-
</div>
380+
</ResponsiveTable>
380381
</div>
381382
)}
382383
</div>

app/analytics/page.tsx

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Nav } from "@/components/nav";
55
import { PortfolioChart, type Timeframe } from "@/components/analytics/PortfolioChart";
66
import { AssetBreakdown } from "@/components/analytics/AssetBreakdown";
77
import { YieldPerformanceSummary } from "@/components/analytics/YieldPerformanceSummary";
8+
import { ResponsiveTable } from '@/components/ui/responsive-table';
89
import {
910
BarChart3,
1011
TrendingUp,
@@ -215,7 +216,7 @@ export default function AnalyticsPage() {
215216
No active strategies found for this account.
216217
</div>
217218
) : (
218-
<div className="overflow-x-auto">
219+
<ResponsiveTable>
219220
<table className="w-full text-left text-xs" data-testid="strategy-pools-table">
220221
<thead>
221222
<tr className="border-b border-zinc-200 dark:border-zinc-800 text-zinc-500 dark:text-zinc-400 uppercase tracking-wider font-semibold">
@@ -231,28 +232,28 @@ export default function AnalyticsPage() {
231232
<tbody className="divide-y divide-zinc-100 dark:divide-zinc-900">
232233
{pools.map((pool) => (
233234
<tr key={pool.id} className="hover:bg-zinc-50/50 dark:hover:bg-zinc-900/50 transition-colors">
234-
<td className="py-3 px-3 font-semibold text-zinc-900 dark:text-zinc-100 flex items-center gap-2">
235+
<td data-label="Strategy / Pool" className="py-3 px-3 font-semibold text-zinc-900 dark:text-zinc-100 flex items-center gap-2">
235236
{pool.name}
236237
{pool.status === "Boosting" && (
237238
<span className="rounded bg-amber-100 dark:bg-amber-950/80 px-1.5 py-0.5 text-[10px] font-bold text-amber-700 dark:text-amber-400">
238239
2x Boost
239240
</span>
240241
)}
241242
</td>
242-
<td className="py-3 px-3 text-zinc-500 dark:text-zinc-400">{pool.category}</td>
243-
<td className="py-3 px-3 text-right font-semibold text-zinc-900 dark:text-zinc-100">
243+
<td data-label="Category" className="py-3 px-3 text-zinc-500 dark:text-zinc-400">{pool.category}</td>
244+
<td data-label="Staked Balance" className="py-3 px-3 text-right font-semibold text-zinc-900 dark:text-zinc-100">
244245
{formatCurrency(pool.stakedBalance)}
245246
</td>
246-
<td className="py-3 px-3 text-right font-semibold text-emerald-600 dark:text-emerald-400">
247+
<td data-label="Current APY" className="py-3 px-3 text-right font-semibold text-emerald-600 dark:text-emerald-400">
247248
{pool.apy}%
248249
</td>
249-
<td className="py-3 px-3 text-right font-medium text-emerald-600 dark:text-emerald-400">
250+
<td data-label="30D Returns" className="py-3 px-3 text-right font-medium text-emerald-600 dark:text-emerald-400">
250251
+{formatCurrency(pool.return30D)}
251252
</td>
252-
<td className="py-3 px-3 text-right font-bold text-zinc-900 dark:text-zinc-100">
253+
<td data-label="Unclaimed Yield" className="py-3 px-3 text-right font-bold text-zinc-900 dark:text-zinc-100">
253254
{formatCurrency(pool.unclaimedYield)}
254255
</td>
255-
<td className="py-3 px-3 text-right">
256+
<td data-label="Action" className="py-3 px-3 text-right">
256257
<button
257258
type="button"
258259
onClick={() => handleClaimPool(pool.id)}
@@ -271,7 +272,7 @@ export default function AnalyticsPage() {
271272
))}
272273
</tbody>
273274
</table>
274-
</div>
275+
</ResponsiveTable>
275276
)}
276277
</div>
277278
</main>

app/globals.css

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,63 @@ body {
6666
}
6767
}
6868
}
69+
70+
/* ── Responsive table ──────────────────────────────────────────────────── */
71+
/* On screens narrower than sm (640px), each row becomes a card. */
72+
.responsive-table-wrapper table {
73+
width: 100%;
74+
border-collapse: collapse;
75+
}
76+
77+
@media (max-width: 639px) {
78+
.responsive-table-wrapper {
79+
display: block;
80+
}
81+
82+
.responsive-table-wrapper thead {
83+
position: absolute;
84+
width: 1px;
85+
height: 1px;
86+
margin: -1px;
87+
padding: 0;
88+
overflow: hidden;
89+
clip: rect(0, 0, 0, 0);
90+
white-space: nowrap;
91+
border: 0;
92+
}
93+
94+
.responsive-table-wrapper tbody,
95+
.responsive-table-wrapper tr,
96+
.responsive-table-wrapper td {
97+
display: block;
98+
}
99+
100+
.responsive-table-wrapper tr {
101+
margin-bottom: 1rem;
102+
border: 1px solid hsl(var(--border));
103+
border-radius: 0.5rem;
104+
padding: 0.75rem;
105+
background-color: hsl(var(--card));
106+
}
107+
108+
.responsive-table-wrapper td {
109+
padding: 0.375rem 0;
110+
border: none !important;
111+
text-align: left !important;
112+
}
113+
114+
.responsive-table-wrapper td::before {
115+
content: attr(data-label);
116+
display: block;
117+
font-size: 0.75rem;
118+
font-weight: 600;
119+
text-transform: uppercase;
120+
letter-spacing: 0.05em;
121+
color: hsl(var(--muted-foreground));
122+
margin-bottom: 0.125rem;
123+
}
124+
125+
.responsive-table-wrapper td[data-label=""]::before {
126+
display: none;
127+
}
128+
}

components/ui/responsive-table.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"use client";
2+
3+
import React from "react";
4+
5+
/**
6+
* A responsive table wrapper that renders as a normal table on desktop
7+
* (≥sm breakpoint) and transforms to a stacked card layout on mobile.
8+
*
9+
* Usage: wrap your existing `<table>` with this component.
10+
* Each `<td>` should have a `data-label` attribute whose value is the
11+
* column name — it will be shown as a label on mobile.
12+
*
13+
* @example
14+
* ```tsx
15+
* <ResponsiveTable>
16+
* <table>
17+
* <thead>
18+
* <tr>
19+
* <th>Name</th>
20+
* <th>Role</th>
21+
* </tr>
22+
* </thead>
23+
* <tbody>
24+
* <tr>
25+
* <td data-label="Name">Alice</td>
26+
* <td data-label="Role">Admin</td>
27+
* </tr>
28+
* </tbody>
29+
* </table>
30+
* </ResponsiveTable>
31+
* ```
32+
*/
33+
export function ResponsiveTable({ children }: { children: React.ReactNode }) {
34+
return <div className="responsive-table-wrapper">{children}</div>;
35+
}

0 commit comments

Comments
 (0)