diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 23a7b4f..2a7edcf 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -85,7 +85,7 @@ export default function App() { +
diff --git a/frontend/src/pages/TransactionsPage.tsx b/frontend/src/pages/TransactionsPage.tsx index 3015b7d..3ab4bfd 100644 --- a/frontend/src/pages/TransactionsPage.tsx +++ b/frontend/src/pages/TransactionsPage.tsx @@ -1,4 +1,5 @@ import { useState, useEffect, useMemo } from "react"; +import { motion, AnimatePresence } from "framer-motion"; import { getStats, type Stats, type DepositSource } from "../api"; type TxDirection = "in" | "out"; @@ -16,10 +17,17 @@ function formatSource(s: DepositSource): TxType { return s === "cashu" ? "cashu" : "lightning"; } +type DirectionFilter = "all" | "in" | "out"; +type TypeFilter = "all" | "lightning" | "cashu"; +type SortOrder = "newest" | "oldest"; + export function TransactionsPage() { const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [directionFilter, setDirectionFilter] = useState("all"); + const [typeFilter, setTypeFilter] = useState("all"); + const [sortOrder, setSortOrder] = useState("newest"); useEffect(() => { let cancelled = false; @@ -41,7 +49,6 @@ export function TransactionsPage() { }, []); const n = (v: number | undefined | null) => Number(v ?? 0).toLocaleString(); - /** Display amount: backend may have stored incoming Lightning in msats; show sats. */ const displaySats = (tx: UnifiedTx): number => { const a = tx.amount_sats; if (tx.direction === "in" && tx.type === "lightning" && a >= 1000) return Math.floor(a / 1000); @@ -75,68 +82,189 @@ export function TransactionsPage() { return merged.slice(0, 50); }, [stats]); - return ( -
-

Transactions

-

- Incoming (deposits) and outgoing (faucet payouts). Lightning and Cashu. -

+ const filteredAndSorted = useMemo(() => { + let list = [...transactions]; + if (directionFilter !== "all") { + list = list.filter((tx) => tx.direction === directionFilter); + } + if (typeFilter !== "all") { + list = list.filter((tx) => tx.type === typeFilter); + } + if (sortOrder === "oldest") { + list = [...list].reverse(); + } + return list; + }, [transactions, directionFilter, typeFilter, sortOrder]); + return ( +
+ {/* Desktop layout: container + hierarchy */} +
+

Transactions

+

+ Incoming (deposits) and outgoing (faucet payouts). Lightning and Cashu. +

+
+ + {/* Filters section */} + {!loading && !error && ( +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ )} + + {/* Loading skeleton */} {loading && ( -
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
)} {error && ( -
+

{error}

)} {!loading && !error && ( -
- {transactions.length === 0 ? ( -

No transactions yet.

+
+ {filteredAndSorted.length === 0 ? ( +
+

No transactions yet

+

+ When you deposit or claim, your transactions will show up here. +

+
) : ( - - - - - - - - - - - - {transactions.map((tx, i) => ( - - - - - - - - ))} - -
DateDirectionTypeAmountDetails
{formatDate(tx.at)} - - {tx.direction === "in" ? "In" : "Out"} + <> + {/* Desktop: grid table */} +
+
+ Date + Direction + Type + Amount + Details +
+ + {filteredAndSorted.map((tx, i) => ( + + {formatDate(tx.at)} + + + {tx.direction === "in" ? "In" : "Out"} + -
- - {tx.type === "cashu" ? "Cashu" : "Lightning"} + + + {tx.type === "cashu" ? "Cashu" : "Lightning"} + - {n(displaySats(tx))} sats{tx.details}
+ {n(displaySats(tx))} sats + {tx.details} + + ))} + +
+ + {/* Mobile: stacked cards */} +
+ + {filteredAndSorted.map((tx, i) => ( + +
+ {formatDate(tx.at)} + {n(displaySats(tx))} sats +
+
+ + {tx.direction === "in" ? "In" : "Out"} + + + {tx.type === "cashu" ? "Cashu" : "Lightning"} + +
+
+ {tx.details} +
+
+ ))} +
+
+ )}
)} + +
); } diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index b10841b..6415033 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -238,6 +238,7 @@ body { .main { min-width: 0; } } .main--full { max-width: 800px; margin: 0 auto; } +.container--transactions .main--full { max-width: none; } .container--single { justify-content: center; } /* Site footer */ @@ -273,67 +274,142 @@ body { margin: 0; } -/* Transactions page */ -.transactions-page { - padding: 0 0 32px; +/* ---- Transactions page (desktop layout) ---- */ +.tx-page { + max-width: 1100px; + margin: 0 auto; + padding: 80px 32px; } -.transactions-title { - font-size: 1.75rem; +.tx-page-header { + margin-bottom: 64px; +} +.tx-page-title { + font-size: 2rem; font-weight: 600; color: var(--text); - margin-bottom: 8px; + margin-bottom: 24px; } -.transactions-intro { - font-size: 15px; +.tx-page-subtitle { + font-size: 16px; color: var(--text-muted); - margin-bottom: 28px; + opacity: 0.9; + margin-bottom: 48px; } -.transactions-box { +.tx-page-bottom { + height: 80px; +} + +/* Filters section */ +.tx-filters { + margin-bottom: 32px; +} +.tx-filters-row { + display: flex; + flex-wrap: wrap; + gap: 24px; + align-items: flex-end; +} +.tx-filter-group { + display: flex; + flex-direction: column; + gap: 6px; +} +.tx-filter-label { + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-soft); +} +.tx-filter-select { + min-width: 140px; + padding: 10px 14px; + font-size: 14px; background: var(--bg-card); border: 1px solid var(--border); - border-radius: var(--radius-card); - padding: 24px; - box-shadow: var(--shadow); - overflow-x: auto; + border-radius: 10px; + color: var(--text); + cursor: pointer; } -.transactions-table { - width: 100%; - border-collapse: collapse; - font-size: 14px; +.tx-filter-select:focus { + outline: none; + border-color: var(--accent); } -.transactions-table th, -.transactions-table td { - padding: 12px 16px; - text-align: left; - border-bottom: 1px solid var(--border); + +/* Transactions card (desktop) */ +.tx-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 16px; + padding: 40px; + min-height: 420px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25); } -.transactions-table th { + +/* Desktop: grid table (hidden on mobile) */ +.tx-table-wrap { + display: none; +} +@media (min-width: 768px) { + .tx-table-wrap { + display: block; + } +} +.tx-table-header { + display: grid; + grid-template-columns: 180px 100px 140px 140px 1fr; + column-gap: 32px; + padding: 0 0 24px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-soft); + align-items: center; } -.transactions-table tbody tr:last-child td { - border-bottom: none; +.tx-table-row { + display: grid; + grid-template-columns: 180px 100px 140px 140px 1fr; + column-gap: 32px; + min-height: 72px; + align-items: center; + padding: 0; + margin-bottom: 20px; + font-size: 14px; + border-radius: 10px; + transition: background 0.15s ease; } -.transactions-table tbody tr:hover td { +.tx-table-row:last-child { + margin-bottom: 0; +} +.tx-table-row:hover { background: var(--bg-card-hover); } -.transactions-amount { - font-variant-numeric: tabular-nums; - color: var(--accent-soft); - font-weight: 500; +.tx-table-row--alt { + background: rgba(255, 255, 255, 0.02); } -.transactions-pubkey, -.transactions-details { +.tx-table-row--alt:hover { + background: var(--bg-card-hover); +} +.tx-td-date { + color: var(--text-soft); + font-size: 13px; +} +.tx-td-amount { + font-size: 1.0625rem; + font-weight: 600; + color: var(--accent-soft); + font-variant-numeric: tabular-nums; +} +.tx-td-details { font-family: ui-monospace, monospace; font-size: 13px; color: var(--text-muted); word-break: break-all; } -.transactions-direction, -.transactions-type { + +/* Badges (direction / type) */ +.tx-badge { display: inline-block; padding: 4px 10px; border-radius: 6px; @@ -341,36 +417,142 @@ body { font-weight: 600; text-transform: capitalize; } -.transactions-direction--in { +.tx-badge--direction.tx-badge--in { background: rgba(34, 197, 94, 0.15); color: var(--accent-soft); } -.transactions-direction--out { +.tx-badge--direction.tx-badge--out { background: rgba(249, 115, 22, 0.15); color: var(--accent); } -.transactions-type--lightning { +.tx-badge--type.tx-badge--lightning { background: rgba(251, 191, 36, 0.15); color: #fbbf24; } -.transactions-type--cashu { +.tx-badge--type.tx-badge--cashu { background: rgba(168, 85, 247, 0.15); color: #a855f7; } -.transactions-loading { - max-width: 400px; + +/* Mobile: stacked cards (visible only on mobile) */ +.tx-cards-mobile { + display: block; } -.transactions-error { +@media (min-width: 768px) { + .tx-cards-mobile { + display: none; + } +} +.tx-mobile-card { + padding: 20px; + border-radius: 14px; + margin-bottom: 16px; + background: var(--bg); + border: 1px solid var(--border); +} +.tx-mobile-card:last-child { + margin-bottom: 0; +} +.tx-mobile-row1 { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} +.tx-mobile-date { + font-size: 13px; + color: var(--text-soft); +} +.tx-mobile-amount { + font-size: 1.25rem; + font-weight: 600; + color: var(--accent-soft); + font-variant-numeric: tabular-nums; +} +.tx-mobile-row2 { + display: flex; + gap: 8px; + margin-bottom: 10px; +} +.tx-mobile-row3 .tx-mobile-details { + font-size: 12px; + color: var(--text-muted); + font-family: ui-monospace, monospace; + word-break: break-all; +} + +/* Empty state */ +.tx-empty { + text-align: center; + padding: 48px 24px; +} +.tx-empty-title { + font-size: 1.125rem; + font-weight: 600; + color: var(--text); + margin-bottom: 8px; +} +.tx-empty-desc { + font-size: 15px; + color: var(--text-muted); + margin: 0; + max-width: 320px; + margin-left: auto; + margin-right: auto; +} + +/* Loading skeleton */ +.tx-loading { + margin-bottom: 0; +} +.tx-skeleton-card { + padding: 20px; + border-radius: 14px; + margin-bottom: 16px; + background: var(--bg-card); + border: 1px solid var(--border); +} +.tx-skeleton-line { + height: 14px; + background: linear-gradient(90deg, var(--border) 25%, var(--bg-card-hover) 50%, var(--border) 75%); + background-size: 200% 100%; + animation: skeleton-shine 1.2s ease-in-out infinite; + border-radius: 4px; + margin-bottom: 10px; +} +.tx-skeleton-line:last-child { + margin-bottom: 0; +} +.tx-skeleton-date { + width: 60%; + height: 12px; +} +.tx-skeleton-amount { + width: 45%; + height: 20px; + margin-left: auto; + margin-bottom: 12px; +} +.tx-skeleton-badges { + width: 70%; + height: 12px; +} +.tx-skeleton-details { + width: 90%; + height: 12px; +} + +/* Error state */ +.tx-error { padding: 20px; background: var(--error-bg); border: 1px solid rgba(248, 113, 113, 0.3); - border-radius: var(--radius-card); + border-radius: 14px; color: var(--error); } -.transactions-empty { - color: var(--text-muted); - font-size: 15px; +.tx-error p { margin: 0; + font-size: 15px; } .header { @@ -2041,8 +2223,19 @@ h1 { .site-nav-link { padding: 8px 12px; font-size: 13px; } .site-footer { padding: 20px 16px 24px; } .site-footer-nav { flex-wrap: wrap; justify-content: center; gap: 16px; } - .transactions-table th, - .transactions-table td { padding: 10px 12px; font-size: 13px; } + + .tx-page { + padding: 48px 24px; + } + .tx-filters-row { + flex-direction: column; + align-items: stretch; + gap: 16px; + } + .tx-filter-select { + min-width: 0; + width: 100%; + } .container { padding: var(--space-sm) var(--space-sm) var(--space-md); @@ -2073,6 +2266,27 @@ h1 { } @media (max-width: 480px) { + .tx-page { + padding: 24px 20px; + } + .tx-page-header { + margin-bottom: 40px; + } + .tx-page-title { + font-size: 1.5rem; + margin-bottom: 16px; + } + .tx-page-subtitle { + margin-bottom: 32px; + } + .tx-card { + padding: 24px 20px; + min-height: 280px; + } + .tx-page-bottom { + height: 48px; + } + .container { padding: 12px 12px 20px; padding-bottom: max(20px, env(safe-area-inset-bottom));