Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions app/[communitySlug]/admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useParams } from 'next/navigation';
import { config } from '@/lib/config';
import { isApiError } from '@/lib/api/errors';
import { queryKeys } from '@/lib/query';
import { ResponsiveTable } from '@/components/ui/responsive-table';
import { EmptyState, ErrorState, LoadingState, safeErrorMessage } from '@/components/ui/api-states';
import { AddressText } from '@/components/wallet/address-text';
import { AdminGuard } from '@/components/admin-guard';
Expand Down Expand Up @@ -284,7 +285,7 @@ function WebhookLogsContent() {
/>
) : (
<div className="border border-border rounded-lg overflow-hidden bg-card">
<div className="overflow-x-auto">
<ResponsiveTable>
<table className="min-w-full divide-y divide-border text-left text-sm">
<thead className="bg-muted text-muted-foreground uppercase text-xs font-semibold tracking-wider">
<tr>
Expand All @@ -304,17 +305,17 @@ function WebhookLogsContent() {
className="hover:bg-muted/50 transition-colors cursor-pointer"
onClick={() => toggleExpand(evt.id)}
>
<td className="px-6 py-4 whitespace-nowrap text-muted-foreground">
<td data-label="" className="px-6 py-4 whitespace-nowrap text-muted-foreground">
<span className="inline-block transition-transform duration-200"
style={{ transform: expandedId === evt.id ? 'rotate(90deg)' : 'rotate(0deg)' }}
>
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-muted-foreground font-mono text-xs">
<td data-label="Timestamp" className="px-6 py-4 whitespace-nowrap text-muted-foreground font-mono text-xs">
{new Date(evt.timestamp).toLocaleString()}
</td>
<td className="px-6 py-4 font-semibold font-mono text-xs text-foreground">
<td data-label="Event Type" className="px-6 py-4 font-semibold font-mono text-xs text-foreground">
<div className="flex items-center gap-2">
<span>{evt.eventType}</span>
{evt.isReplay && (
Expand All @@ -324,15 +325,15 @@ function WebhookLogsContent() {
)}
</div>
</td>
<td className="px-6 py-4 text-muted-foreground font-mono text-xs">
<td data-label="Target" className="px-6 py-4 text-muted-foreground font-mono text-xs">
<AddressText
address={evt.affectedIdentifier}
label="Target address or resource"
announceInvalid={false}
className="text-muted-foreground"
/>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<td data-label="Status" className="px-6 py-4 whitespace-nowrap">
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold tracking-wide uppercase ${
evt.status === 'success'
Expand All @@ -345,11 +346,11 @@ function WebhookLogsContent() {
{evt.status}
</span>
</td>
<td className="px-6 py-4 text-muted-foreground text-xs max-w-xs truncate font-mono">
<td data-label="Payload" className="px-6 py-4 text-muted-foreground text-xs max-w-xs truncate font-mono">
{JSON.stringify(evt.payloadSummary)}
</td>
{isMockMode && (
<td className="px-6 py-4 whitespace-nowrap">
<td data-label="Actions" className="px-6 py-4 whitespace-nowrap">
<Button
size="sm"
variant="outline"
Expand All @@ -376,7 +377,7 @@ function WebhookLogsContent() {
))}
</tbody>
</table>
</div>
</ResponsiveTable>
</div>
)}
</div>
Expand Down
19 changes: 10 additions & 9 deletions app/analytics/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Nav } from "@/components/nav";
import { PortfolioChart, type Timeframe } from "@/components/analytics/PortfolioChart";
import { AssetBreakdown } from "@/components/analytics/AssetBreakdown";
import { YieldPerformanceSummary } from "@/components/analytics/YieldPerformanceSummary";
import { ResponsiveTable } from '@/components/ui/responsive-table';
import {
BarChart3,
TrendingUp,
Expand Down Expand Up @@ -215,7 +216,7 @@ export default function AnalyticsPage() {
No active strategies found for this account.
</div>
) : (
<div className="overflow-x-auto">
<ResponsiveTable>
<table className="w-full text-left text-xs" data-testid="strategy-pools-table">
<thead>
<tr className="border-b border-zinc-200 dark:border-zinc-800 text-zinc-500 dark:text-zinc-400 uppercase tracking-wider font-semibold">
Expand All @@ -231,28 +232,28 @@ export default function AnalyticsPage() {
<tbody className="divide-y divide-zinc-100 dark:divide-zinc-900">
{pools.map((pool) => (
<tr key={pool.id} className="hover:bg-zinc-50/50 dark:hover:bg-zinc-900/50 transition-colors">
<td className="py-3 px-3 font-semibold text-zinc-900 dark:text-zinc-100 flex items-center gap-2">
<td data-label="Strategy / Pool" className="py-3 px-3 font-semibold text-zinc-900 dark:text-zinc-100 flex items-center gap-2">
{pool.name}
{pool.status === "Boosting" && (
<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">
2x Boost
</span>
)}
</td>
<td className="py-3 px-3 text-zinc-500 dark:text-zinc-400">{pool.category}</td>
<td className="py-3 px-3 text-right font-semibold text-zinc-900 dark:text-zinc-100">
<td data-label="Category" className="py-3 px-3 text-zinc-500 dark:text-zinc-400">{pool.category}</td>
<td data-label="Staked Balance" className="py-3 px-3 text-right font-semibold text-zinc-900 dark:text-zinc-100">
{formatCurrency(pool.stakedBalance)}
</td>
<td className="py-3 px-3 text-right font-semibold text-emerald-600 dark:text-emerald-400">
<td data-label="Current APY" className="py-3 px-3 text-right font-semibold text-emerald-600 dark:text-emerald-400">
{pool.apy}%
</td>
<td className="py-3 px-3 text-right font-medium text-emerald-600 dark:text-emerald-400">
<td data-label="30D Returns" className="py-3 px-3 text-right font-medium text-emerald-600 dark:text-emerald-400">
+{formatCurrency(pool.return30D)}
</td>
<td className="py-3 px-3 text-right font-bold text-zinc-900 dark:text-zinc-100">
<td data-label="Unclaimed Yield" className="py-3 px-3 text-right font-bold text-zinc-900 dark:text-zinc-100">
{formatCurrency(pool.unclaimedYield)}
</td>
<td className="py-3 px-3 text-right">
<td data-label="Action" className="py-3 px-3 text-right">
<button
type="button"
onClick={() => handleClaimPool(pool.id)}
Expand All @@ -271,7 +272,7 @@ export default function AnalyticsPage() {
))}
</tbody>
</table>
</div>
</ResponsiveTable>
)}
</div>
</main>
Expand Down
60 changes: 60 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,63 @@ body {
}
}
}

/* ── Responsive table ──────────────────────────────────────────────────── */
/* On screens narrower than sm (640px), each row becomes a card. */
.responsive-table-wrapper table {
width: 100%;
border-collapse: collapse;
}

@media (max-width: 639px) {
.responsive-table-wrapper {
display: block;
}

.responsive-table-wrapper thead {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}

.responsive-table-wrapper tbody,
.responsive-table-wrapper tr,
.responsive-table-wrapper td {
display: block;
}

.responsive-table-wrapper tr {
margin-bottom: 1rem;
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
padding: 0.75rem;
background-color: hsl(var(--card));
}

.responsive-table-wrapper td {
padding: 0.375rem 0;
border: none !important;
text-align: left !important;
}

.responsive-table-wrapper td::before {
content: attr(data-label);
display: block;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: hsl(var(--muted-foreground));
margin-bottom: 0.125rem;
}

.responsive-table-wrapper td[data-label=""]::before {
display: none;
}
}
35 changes: 35 additions & 0 deletions components/ui/responsive-table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"use client";

import React from "react";

/**
* A responsive table wrapper that renders as a normal table on desktop
* (≥sm breakpoint) and transforms to a stacked card layout on mobile.
*
* Usage: wrap your existing `<table>` with this component.
* Each `<td>` should have a `data-label` attribute whose value is the
* column name — it will be shown as a label on mobile.
*
* @example
* ```tsx
* <ResponsiveTable>
* <table>
* <thead>
* <tr>
* <th>Name</th>
* <th>Role</th>
* </tr>
* </thead>
* <tbody>
* <tr>
* <td data-label="Name">Alice</td>
* <td data-label="Role">Admin</td>
* </tr>
* </tbody>
* </table>
* </ResponsiveTable>
* ```
*/
export function ResponsiveTable({ children }: { children: React.ReactNode }) {
return <div className="responsive-table-wrapper">{children}</div>;
}