From daf44b2b17fa0136cb86a1c1e93e1cc98c34910e Mon Sep 17 00:00:00 2001 From: ECWireless Date: Wed, 1 Jul 2026 04:01:15 +0000 Subject: [PATCH 1/3] Improve mobile dashboard responsiveness --- .env.example | 2 + .gitignore | 2 + README.md | 32 ++ eslint.config.mjs | 2 + package.json | 3 + playwright.config.ts | 56 +++ pnpm-lock.yaml | 55 ++- src/app/admin/providers/page.tsx | 16 +- .../classification-linked-fields.tsx | 10 +- .../admin/quarters/[id]/transactions/page.tsx | 96 ++--- src/app/admin/treasury-accounts/page.tsx | 16 +- src/app/api/e2e/session/route.ts | 88 +++++ src/app/globals.css | 147 +++++++- src/app/membership/page.tsx | 44 +-- src/app/page.tsx | 7 +- src/app/proposals/page.tsx | 22 +- src/app/raids/page.tsx | 72 ++-- src/app/reports/page.tsx | 6 +- src/app/reports/quarters/[id]/page.tsx | 36 +- src/app/rips/page.tsx | 24 +- src/components/app-header.tsx | 230 +++++++++--- src/components/auth/wallet-connect.tsx | 2 +- src/components/ui/button.tsx | 16 +- tests/e2e/responsive-dashboard.spec.ts | 340 ++++++++++++++++++ 24 files changed, 1103 insertions(+), 221 deletions(-) create mode 100644 playwright.config.ts create mode 100644 src/app/api/e2e/session/route.ts create mode 100644 tests/e2e/responsive-dashboard.spec.ts diff --git a/.env.example b/.env.example index 5e1e26b..bef4fef 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,8 @@ NEXT_PUBLIC_APP_URL=http://localhost:3000 # Optional comma-separated hostnames for proxied Next.js dev server origins. NEXT_ALLOWED_DEV_ORIGINS= +# Enables local Playwright-only mock sessions. Never set in production. +E2E_AUTH_ENABLED=false # Server-only configuration DATABASE_URL= diff --git a/.gitignore b/.gitignore index 7b8da95..d3d2568 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,8 @@ # testing /coverage +/playwright-report +/test-results # next.js /.next/ diff --git a/README.md b/README.md index 9c20d1b..e5ca7e2 100644 --- a/README.md +++ b/README.md @@ -50,11 +50,43 @@ Keep machine-specific proxy hostnames and URLs out of committed source. - `pnpm build`: create a production build. - `pnpm start`: run the production build. - `pnpm lint`: run ESLint. +- `pnpm test:e2e`: run Playwright responsive smoke tests. +- `pnpm test:e2e:install`: install the Playwright Chromium browser. - `pnpm db:generate`: generate Drizzle SQL migrations from the schema. - `pnpm db:migrate`: apply Drizzle migrations to `DATABASE_URL`. - `pnpm db:reset:local`: drop and recreate the local `public` schema, then run migrations. - `pnpm db:studio`: open Drizzle Studio for local database inspection. +## End-to-End QA + +The Playwright suite checks public and gated dashboard routes across mobile, +tablet, and desktop viewports. It uses the local database state that is already +present. Detail-page checks skip with a clear message when the local database +does not contain a matching quarter or report. + +On a fresh Ubuntu server, install browser system dependencies once: + +```bash +pnpm exec playwright install-deps chromium +``` + +Then install the Chromium browser bundle: + +```bash +pnpm test:e2e:install +``` + +Run the suite: + +```bash +pnpm test:e2e +``` + +The tests start the Next.js dev server with `E2E_AUTH_ENABLED=true` and use a +local-only mock session endpoint for member, cleric, and admin route coverage. +That endpoint returns 404 unless `E2E_AUTH_ENABLED=true` and the app is not +running in production. + ## Stack - Next.js App Router. diff --git a/eslint.config.mjs b/eslint.config.mjs index 05e726d..ac9d10f 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -11,6 +11,8 @@ const eslintConfig = defineConfig([ ".next/**", "out/**", "build/**", + "playwright-report/**", + "test-results/**", "next-env.d.ts", ]), ]); diff --git a/package.json b/package.json index 025f3b5..fd5490f 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ "build": "next build", "start": "next start", "lint": "eslint .", + "test:e2e": "playwright test", + "test:e2e:install": "playwright install chromium", "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", "db:reset:local": "node scripts/reset-local-db.mjs", @@ -43,6 +45,7 @@ "wagmi": "^3.6.16" }, "devDependencies": { + "@playwright/test": "^1.61.1", "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/pg": "^8.18.0", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..03a0396 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,56 @@ +import { defineConfig, devices } from "@playwright/test"; + +const port = Number(process.env.PLAYWRIGHT_PORT ?? 3131); +const host = "127.0.0.1"; +const baseURL = `http://${host}:${port}`; + +export default defineConfig({ + expect: { + timeout: 10_000, + }, + fullyParallel: false, + outputDir: "test-results/e2e", + reporter: [["list"], ["html", { open: "never" }]], + testDir: "./tests/e2e", + timeout: 45_000, + use: { + baseURL, + trace: "retain-on-failure", + }, + webServer: { + command: `E2E_AUTH_ENABLED=true COREPACK_HOME=/tmp/corepack corepack pnpm exec next dev -H ${host} -p ${port}`, + reuseExistingServer: false, + timeout: 120_000, + url: baseURL, + }, + projects: [ + { + name: "mobile", + use: { + ...devices["Pixel 5"], + viewport: { height: 844, width: 390 }, + }, + }, + { + name: "small-mobile", + use: { + ...devices["Pixel 5"], + viewport: { height: 740, width: 360 }, + }, + }, + { + name: "tablet", + use: { + ...devices["iPad (gen 7)"], + browserName: "chromium", + viewport: { height: 1024, width: 768 }, + }, + }, + { + name: "desktop", + use: { + viewport: { height: 900, width: 1440 }, + }, + }, + ], +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fe40f26..7401d8d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,7 +19,7 @@ importers: version: 5.100.14(react@19.2.4) '@vercel/analytics': specifier: ^2.0.1 - version: 2.0.1(next@16.2.6(@babel/core@7.29.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) + version: 2.0.1(next@16.2.6(@babel/core@7.29.7)(@playwright/test@1.61.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) '@x402/core': specifier: ^2.17.0 version: 2.17.0 @@ -28,7 +28,7 @@ importers: version: 2.17.0(typescript@5.9.3) '@x402/next': specifier: ^2.17.0 - version: 2.17.0(ethers@6.16.0)(next@16.2.6(@babel/core@7.29.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) + version: 2.17.0(ethers@6.16.0)(next@16.2.6(@babel/core@7.29.7)(@playwright/test@1.61.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -49,7 +49,7 @@ importers: version: 1.17.0(react@19.2.4) next: specifier: 16.2.6 - version: 16.2.6(@babel/core@7.29.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 16.2.6(@babel/core@7.29.7)(@playwright/test@1.61.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) pg: specifier: ^8.18.0 version: 8.18.0 @@ -78,6 +78,9 @@ importers: specifier: ^3.6.16 version: 3.6.16(@tanstack/query-core@5.100.14)(@tanstack/react-query@5.100.14(react@19.2.4))(@types/react@19.2.15)(react@19.2.4)(typescript@5.9.3)(viem@2.51.3(typescript@5.9.3)(zod@4.4.3)) devDependencies: + '@playwright/test': + specifier: ^1.61.1 + version: 1.61.1 '@tailwindcss/postcss': specifier: ^4 version: 4.3.0 @@ -1017,6 +1020,11 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} + '@playwright/test@1.61.1': + resolution: {integrity: sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==} + engines: {node: '>=18'} + hasBin: true + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -2125,6 +2133,11 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2808,6 +2821,16 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + playwright-core@1.61.1: + resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.61.1: + resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==} + engines: {node: '>=18'} + hasBin: true + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -3995,6 +4018,10 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} + '@playwright/test@1.61.1': + dependencies: + playwright: 1.61.1 + '@rtsao/scc@1.1.0': {} '@scure/base@1.2.6': {} @@ -4316,9 +4343,9 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.12.2': optional: true - '@vercel/analytics@2.0.1(next@16.2.6(@babel/core@7.29.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)': + '@vercel/analytics@2.0.1(next@16.2.6(@babel/core@7.29.7)(@playwright/test@1.61.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)': optionalDependencies: - next: 16.2.6(@babel/core@7.29.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + next: 16.2.6(@babel/core@7.29.7)(@playwright/test@1.61.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: 19.2.4 '@wagmi/connectors@8.0.15(@wagmi/core@3.5.0(@tanstack/query-core@5.100.14)(@types/react@19.2.15)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.51.3(typescript@5.9.3)(zod@4.4.3)))(typescript@5.9.3)(viem@2.51.3(typescript@5.9.3)(zod@4.4.3))': @@ -4374,11 +4401,11 @@ snapshots: - typescript - utf-8-validate - '@x402/next@2.17.0(ethers@6.16.0)(next@16.2.6(@babel/core@7.29.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)': + '@x402/next@2.17.0(ethers@6.16.0)(next@16.2.6(@babel/core@7.29.7)(@playwright/test@1.61.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)': dependencies: '@x402/core': 2.17.0 '@x402/extensions': 2.17.0(ethers@6.16.0)(typescript@5.9.3) - next: 16.2.6(@babel/core@7.29.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + next: 16.2.6(@babel/core@7.29.7)(@playwright/test@1.61.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) transitivePeerDependencies: - bufferutil - ethers @@ -5244,6 +5271,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -5725,7 +5755,7 @@ snapshots: natural-compare@1.4.0: {} - next@16.2.6(@babel/core@7.29.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + next@16.2.6(@babel/core@7.29.7)(@playwright/test@1.61.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@next/env': 16.2.6 '@swc/helpers': 0.5.15 @@ -5744,6 +5774,7 @@ snapshots: '@next/swc-linux-x64-musl': 16.2.6 '@next/swc-win32-arm64-msvc': 16.2.6 '@next/swc-win32-x64-msvc': 16.2.6 + '@playwright/test': 1.61.1 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' @@ -5914,6 +5945,14 @@ snapshots: picomatch@4.0.4: {} + playwright-core@1.61.1: {} + + playwright@1.61.1: + dependencies: + playwright-core: 1.61.1 + optionalDependencies: + fsevents: 2.3.2 + possible-typed-array-names@1.1.0: {} postcss@8.4.31: diff --git a/src/app/admin/providers/page.tsx b/src/app/admin/providers/page.tsx index 77d18b9..3b129bb 100644 --- a/src/app/admin/providers/page.tsx +++ b/src/app/admin/providers/page.tsx @@ -479,8 +479,8 @@ function ProviderRankingTable({ {providers.length > 0 ? ( -
- +
+
@@ -501,7 +501,7 @@ function ProviderRankingTable({ key={provider.id} className="transition-colors hover:bg-muted/50" > - - - - - -
Provider + + + + + + @@ -179,7 +179,7 @@ export function ClassificationLinkedFields({ name={counterpartyDisabled ? undefined : "counterpartyEntityId"} defaultValue={defaultCounterpartyEntityId ?? ""} disabled={counterpartyDisabled} - className="h-9 rounded-md border border-input bg-background px-3 text-sm disabled:cursor-not-allowed disabled:bg-secondary disabled:text-muted-foreground" + className="h-9 w-full min-w-0 rounded-md border border-input bg-background px-3 text-sm disabled:cursor-not-allowed disabled:bg-secondary disabled:text-muted-foreground" > diff --git a/src/app/admin/quarters/[id]/transactions/page.tsx b/src/app/admin/quarters/[id]/transactions/page.tsx index 522a1e9..7add6e3 100644 --- a/src/app/admin/quarters/[id]/transactions/page.tsx +++ b/src/app/admin/quarters/[id]/transactions/page.tsx @@ -248,26 +248,26 @@ function QuarterBalancesPanel({
-
-
+
+

Total

{balances.length} account{balances.length === 1 ? "" : "s"}

-
+

Opening

{formatCurrencyNumber(totalBalance.openingUsd)}

-
+

Closing

{formatCurrencyNumber(totalBalance.closingUsd)}

-
+

Net Change

{formatCurrencyNumber(totalBalance.netChangeUsd)} @@ -286,7 +286,7 @@ function QuarterBalancesPanel({ key={`${balance.chainId}:${balance.accountAddress}`} className="rounded-md border border-border bg-background" > -

+

{balance.accountName}

-
+

Opening

{formatCurrencyNumber(balance.openingUsd)}

-
+

Closing

{formatCurrencyNumber(balance.closingUsd)}

-
+

Net Change

@@ -315,8 +315,8 @@ function QuarterBalancesPanel({

-
- +
+
@@ -347,19 +347,19 @@ function QuarterBalancesPanel({ return ( - - + - - -
Asset
{symbol} + {symbol} {opening ? formatTokenAmount(opening.balance) : "-"} + {closing ? formatTokenAmount(closing.balance) : "-"} + {opening ? formatCurrencyNumber(Number(opening.usdValue)) : "-"} + {closing ? formatCurrencyNumber(Number(closing.usdValue)) : "-"} @@ -472,8 +472,8 @@ function BalanceValidationPanel({ ) : null} {topVariances.length > 0 ? ( -
- +
+
@@ -490,25 +490,25 @@ function BalanceValidationPanel({ - - - - - + + - - @@ -1008,10 +1008,10 @@ function TransferCard({ return (
-
+
{position} / {total} @@ -1089,15 +1089,15 @@ function TransferCard({ {isSwap ? : null}
-
+
{transactionExplorerUrl ? ( - {formatHash(transfer.txHash)} + {formatHash(transfer.txHash)} @@ -1134,7 +1134,7 @@ function TransferCard({ return (
@@ -1287,12 +1287,12 @@ function ManualLedgerEntryCard({ return (
-
+
{position} / {total} @@ -1359,15 +1359,15 @@ function ManualLedgerEntryCard({ {entry.notes ? Notes: {entry.notes} : null}
-
+
{transactionExplorerUrl && explorerTxHash ? ( - {formatHash(explorerTxHash)} + {formatHash(explorerTxHash)} @@ -1552,9 +1552,15 @@ export default async function QuarterTransactionsPage({ syncStatus={toastSyncStatus} /> -
-
-
+
+
+
Quarters -
-
-
+
+
+
-
-

+

+

{formatDate(quarter.startsOn)} - {formatDate(quarter.endsOn)}

-

+

{quarter.label} Transaction Review

diff --git a/src/app/admin/treasury-accounts/page.tsx b/src/app/admin/treasury-accounts/page.tsx index 9f0e25f..f8d0062 100644 --- a/src/app/admin/treasury-accounts/page.tsx +++ b/src/app/admin/treasury-accounts/page.tsx @@ -454,8 +454,8 @@ function AccountRankingTable({
{accounts.length > 0 ? ( -
-
Account
+

{variance.accountName}

+ {variance.assetSymbol} {variance.opening}{variance.movement} + {variance.opening}{variance.movement} {variance.expectedClosing} + {variance.actualClosing} + {variance.difference}
+
+
@@ -476,7 +476,7 @@ function AccountRankingTable({ key={account.id} className="transition-colors hover:bg-muted/50" > - - - - - -
Account + + + + + + null)) as + | E2ESessionRequest + | null; + const role = parseRole(body?.role); + + if (!role) { + return NextResponse.json({ error: "Invalid role" }, { status: 400 }); + } + + const session = await getAuthSession(); + + session.address = E2E_WALLET_ADDRESS; + session.authenticatedAt = new Date().toISOString(); + session.chainId = 100; + session.permissions = getPermissions(role); + session.viewMode = role; + delete session.nonce; + await session.save(); + + return NextResponse.json(serializeSession(session)); +} + +export async function DELETE() { + if (!isE2EAuthEnabled()) { + return NextResponse.json({ error: "Not found" }, { status: 404 }); + } + + const session = await getAuthSession(); + session.destroy(); + + return NextResponse.json({ ok: true }); +} diff --git a/src/app/globals.css b/src/app/globals.css index 85593d6..7a4c06f 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -142,10 +142,16 @@ @layer base { * { @apply border-border outline-ring/50; + box-sizing: border-box; + } + *::before, + *::after { + box-sizing: border-box; } body { @apply bg-background text-foreground; font-family: var(--font-ui); + margin: 0; } html { @apply font-sans; @@ -175,6 +181,26 @@ } } + @keyframes drawer-backdrop-in { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + + @keyframes drawer-panel-in { + from { + opacity: 0.92; + transform: translateX(1rem); + } + to { + opacity: 1; + transform: translateX(0); + } + } + .toast-enter { animation: toast-in 180ms ease-out both; } @@ -183,8 +209,16 @@ animation: toast-out 180ms ease-in both; } + .drawer-backdrop-enter { + animation: drawer-backdrop-in 160ms ease-out both; + } + + .drawer-panel-enter { + animation: drawer-panel-in 180ms ease-out both; + } + .container-custom { - @apply mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8; + @apply mx-auto box-border w-full max-w-7xl px-3 sm:px-6 lg:px-8; } .type-label { @@ -194,4 +228,115 @@ .type-label-sm { @apply text-[0.7rem] font-semibold tracking-normal; } + + .mobile-table-scroll { + @apply -mx-3 overflow-x-auto px-3 sm:mx-0 sm:px-0; + scrollbar-width: thin; + } + + .overflow-wrap-anywhere { + overflow-wrap: anywhere; + } + + .mobile-card-table { + @apply w-full text-left text-sm; + } + + .mobile-card-table thead { + @apply hidden md:table-header-group; + } + + .mobile-card-table tbody { + @apply grid gap-3 md:table-row-group; + } + + .mobile-card-table tr { + @apply grid gap-2 rounded-md border border-border bg-background p-3 md:table-row md:rounded-none md:border-0 md:bg-transparent md:p-0; + } + + .mobile-card-table tbody tr { + @apply md:border-b md:border-border; + } + + .mobile-card-table tbody tr:last-child { + @apply md:border-b-0; + } + + .mobile-card-table td, + .mobile-card-table th { + @apply md:px-4 md:py-3; + } + + .mobile-card-table td { + @apply grid min-w-0 grid-cols-[7.5rem_minmax(0,1fr)] gap-3 py-0 text-right md:table-cell md:text-left; + overflow-wrap: anywhere; + } + + .mobile-card-table td::before { + content: attr(data-label); + @apply text-left text-xs font-semibold text-muted-foreground uppercase md:hidden; + } + + .mobile-card-table td[data-align="right"] { + @apply md:text-right; + } + + .mobile-card-table td[data-full="true"] { + @apply grid-cols-1 text-left; + } + + .mobile-card-table td[data-full="true"]::before { + @apply sr-only; + } + + .mobile-card-table-lg { + @apply w-full text-left text-sm; + } + + .mobile-card-table-lg thead { + @apply hidden lg:table-header-group; + } + + .mobile-card-table-lg tbody { + @apply grid gap-3 lg:table-row-group; + } + + .mobile-card-table-lg tr { + @apply grid gap-2 rounded-md border border-border bg-background p-3 lg:table-row lg:rounded-none lg:border-0 lg:bg-transparent lg:p-0; + } + + .mobile-card-table-lg tbody tr { + @apply lg:border-b lg:border-border; + } + + .mobile-card-table-lg tbody tr:last-child { + @apply lg:border-b-0; + } + + .mobile-card-table-lg td, + .mobile-card-table-lg th { + @apply lg:px-4 lg:py-3; + } + + .mobile-card-table-lg td { + @apply grid min-w-0 grid-cols-[7.5rem_minmax(0,1fr)] gap-3 py-0 text-right lg:table-cell lg:text-left; + overflow-wrap: anywhere; + } + + .mobile-card-table-lg td::before { + content: attr(data-label); + @apply text-left text-xs font-semibold text-muted-foreground uppercase lg:hidden; + } + + .mobile-card-table-lg td[data-align="right"] { + @apply lg:text-right; + } + + .mobile-card-table-lg td[data-full="true"] { + @apply grid-cols-1 text-left; + } + + .mobile-card-table-lg td[data-full="true"]::before { + @apply sr-only; + } } diff --git a/src/app/membership/page.tsx b/src/app/membership/page.tsx index 7f183c2..db35122 100644 --- a/src/app/membership/page.tsx +++ b/src/app/membership/page.tsx @@ -255,8 +255,8 @@ function ClericAccessSection({ {clericRoles.length > 0 ? ( -
- +
+
@@ -271,13 +271,13 @@ function ClericAccessSection({ {clericRoles.map((role) => ( - - - - -
Wallet
+ + + {formatTimestamp(role.createdAt.toISOString())} + {role.revokedAt ? formatTimestamp(role.revokedAt.toISOString()) : "-"} + {role.revokedAt ? null : (
- +
+
@@ -356,23 +356,23 @@ function MembershipActivityTable({ rows }: { rows: MembershipActivityRow[] }) { {rows.map((row) => ( - - - - - - - -
Activity
+ {row.proposalTitle ? ( -

+

{row.proposalTitle}

) : null}
+ {row.recipientAddress && row.recipientAddress.toLowerCase() !== row.memberAddress.toLowerCase() ? ( -
+
) : null}
+ {formatTimestamp(row.executedAt)} + {formatAmount({ amount: row.assetAmount, symbol: row.assetSymbol, })} + {formatCurrency(row.usdAmount)} +

{formatShares(row.shares)}

{row.loot ? (

@@ -400,7 +400,7 @@ function MembershipActivityTable({ rows }: { rows: MembershipActivityRow[] }) {

) : null}
+

{row.quarterLabel ?? "-"}

{row.quarterStatus ? (

@@ -408,8 +408,8 @@ function MembershipActivityTable({ rows }: { rows: MembershipActivityRow[] }) {

) : null}
-
+
+
{row.daohausUrl ? ( DAOhaus ) : null} diff --git a/src/app/page.tsx b/src/app/page.tsx index e7c5ea1..7d56293 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -38,11 +38,10 @@ function PublicHome({ session }: { session: SessionState }) { RaidGuild

Accounting Dashboard diff --git a/src/app/proposals/page.tsx b/src/app/proposals/page.tsx index bea10ba..8ff26cb 100644 --- a/src/app/proposals/page.tsx +++ b/src/app/proposals/page.tsx @@ -81,8 +81,8 @@ function ProposalActivityTable({ rows }: { rows: ProposalActivityRow[] }) { } return ( -

- +
+
@@ -97,13 +97,13 @@ function ProposalActivityTable({ rows }: { rows: ProposalActivityRow[] }) { {rows.map((row) => ( - - - - - - - ); } return ( -
Proposal
-

{row.title}

+
+

{row.title}

{row.proposalNumber ?? row.proposalId}

+

{row.quarterLabel ?? "-"}

{row.quarterStatus ? (

@@ -111,16 +111,16 @@ function ProposalActivityTable({ rows }: { rows: ProposalActivityRow[] }) {

) : null}
+ {formatTimestamp(row.executedAt)} + {formatAmount(row.assetAmount)} {row.assetSymbol} + {formatCurrency(row.usdAmount)} +

{row.category ?? "Unclassified"}

{row.counterpartyName ? (

@@ -128,8 +128,8 @@ function ProposalActivityTable({ rows }: { rows: ProposalActivityRow[] }) {

) : null}
-
+
+
DAOhaus {row.explorerUrl ? ( diff --git a/src/app/raids/page.tsx b/src/app/raids/page.tsx index 0416920..d41e260 100644 --- a/src/app/raids/page.tsx +++ b/src/app/raids/page.tsx @@ -433,22 +433,34 @@ function AccountingSectionHeader({ function TableLinkCell({ children, className = "", + full = false, href, + label, + numeric = false, }: { children: ReactNode; className?: string; + full?: boolean; href?: string | null; + label: string; + numeric?: boolean; }) { + const dataProps = { + "data-align": numeric ? "right" : undefined, + "data-full": full ? "true" : undefined, + "data-label": label, + }; + if (!href) { return ( -
+
{children}
+ {clients.length > 0 ? ( -
- +
+
@@ -504,28 +516,30 @@ function TopClientsTable({ key={client.clientId} className="transition-colors hover:bg-muted/50" > - + {client.clientName} {formatAccountingCurrency(client.revenueCents)} - + {client.raidCount} - + {formatAccountingCurrency(client.expectedSpoilsCents)} - + {formatAccountingCurrency(client.spoilsReceivedCents)} - + {formatAccountingCurrency(client.expectedTeamPoolCents)} - + {formatAccountingCurrency( client.subcontractorPayoutCents, )} @@ -559,8 +573,8 @@ function RaidAccountingTable({ title="Raid Accounting" /> {summaries.length > 0 ? ( -
-
Client
+
+
@@ -595,10 +609,10 @@ function RaidAccountingTable({ key={summary.raidId} className="transition-colors hover:bg-muted/50" > - + {summary.clientName} - +
{summary.raidName} {summary.isShipped ? ( @@ -610,34 +624,36 @@ function RaidAccountingTable({ {formatAccountingCurrency(summary.revenueCents)} - + {formatAccountingCurrency(summary.expectedSpoilsCents)} - + {formatAccountingCurrency(summary.spoilsReceivedCents)} - + {formatAccountingCurrency(summary.remainingSpoilsCents)} - + - + {formatAccountingCurrency(summary.expectedTeamPoolCents)} - + {formatAccountingCurrency( summary.subcontractorPayoutCents, )} - + {formatAccountingCurrency(summary.remainingPoolCents)} - +
@@ -669,8 +685,8 @@ function SubcontractorAccountingTable({ title="Subcontractor Accounting" /> {summaries.length > 0 ? ( -
-
Client
+
+
@@ -690,21 +706,23 @@ function SubcontractorAccountingTable({ key={summary.subcontractorId} className="transition-colors hover:bg-muted/50" > - + {summary.subcontractorName} {formatAccountingCurrency( summary.subcontractorPayoutCents, )} - + {summary.raidCount} - + {summary.isArchived ? ( Archived diff --git a/src/app/reports/page.tsx b/src/app/reports/page.tsx index 620a103..fc0f950 100644 --- a/src/app/reports/page.tsx +++ b/src/app/reports/page.tsx @@ -117,17 +117,17 @@ export default async function ReportsPage() { : "Ready for publication preview"}

-
+
View Report Export XLSX diff --git a/src/app/reports/quarters/[id]/page.tsx b/src/app/reports/quarters/[id]/page.tsx index a30fbc4..3802255 100644 --- a/src/app/reports/quarters/[id]/page.tsx +++ b/src/app/reports/quarters/[id]/page.tsx @@ -123,8 +123,8 @@ function RankedTable({
{rows.length > 0 ? ( -
-
Subcontractor
+
+
@@ -134,21 +134,21 @@ function RankedTable({ - - + - {rows.map((row) => ( - - - + @@ -191,8 +191,8 @@ function BalanceTable({ {balances.length > 0 ? ( -
-
Name
Total + Total {totals.entries} + {formatCurrency(totals.totalUsd)}
+ {row.label} {row.entries} + {row.entries} {formatCurrency(row.totalUsd)}
+
+
@@ -205,33 +205,33 @@ function BalanceTable({ - - + - - {balances.map((balance) => ( - - - - diff --git a/src/app/rips/page.tsx b/src/app/rips/page.tsx index 30522ba..2c20692 100644 --- a/src/app/rips/page.tsx +++ b/src/app/rips/page.tsx @@ -239,8 +239,8 @@ function RipTable({ rips }: { rips: RipView[] }) { {rips.length} RIP{rips.length === 1 ? "" : "s"} -
-
Account
Total + Total {formatCurrency(total.openingUsd)} + {formatCurrency(total.closingUsd)} + {formatCurrency(total.netChangeUsd)}
+

{balance.accountName}

+ {formatCurrency(balance.openingUsd)} + {formatCurrency(balance.closingUsd)} + {formatCurrency(balance.netChangeUsd)}
+
+
@@ -258,47 +258,47 @@ function RipTable({ rips }: { rips: RipView[] }) { return ( - - - - -
RIP
+ {rip.title} + {formatTimestamp(rip.createdAt)} + {formatCurrency(rip.totalUsd)} + {rip.entryCount} +