diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/dashboard/Dashboard.tsx b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/dashboard/Dashboard.tsx index 6e2841b9..b85a4ff6 100644 --- a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/dashboard/Dashboard.tsx +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/dashboard/Dashboard.tsx @@ -5,6 +5,7 @@ import { Globe } from '@/pages/globe/Globe' import { BTCPanel } from '@/pages/panels/BTCPanel' import { CVEVelocityPanel } from '@/pages/panels/CVEVelocityPanel' import { DShieldPanel } from '@/pages/panels/DShieldPanel' +import { ETHPanel } from '@/pages/panels/ETHPanel' import { KEVPanel } from '@/pages/panels/KEVPanel' import { RansomwarePanel } from '@/pages/panels/RansomwarePanel' import { useUIStore } from '@/stores/ui' @@ -29,6 +30,7 @@ export function Dashboard(): React.ReactElement {
diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/ETHPanel.module.scss b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/ETHPanel.module.scss new file mode 100644 index 00000000..d393c3a0 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/ETHPanel.module.scss @@ -0,0 +1,59 @@ +// ©AngelaMos | 2026 +// ETHPanel.module.scss + +.hero { + display: flex; + align-items: baseline; + gap: 6px; + padding: 8px; +} + +.price { + font-family: var(--font-mono); + font-size: var(--type-num-xl); + font-variant-numeric: tabular-nums; + color: var(--fg-1); + line-height: 1; +} + +.unit { + font-size: var(--type-label); + letter-spacing: var(--letter-spacing-label); + color: var(--fg-3); + text-transform: uppercase; +} + +.changes { + padding: 0 8px; +} + +.change { + display: grid; + grid-template-columns: 36px auto; + gap: 8px; + align-items: center; + font-family: var(--font-mono); + font-size: var(--type-body); + padding: var(--row-py) 0; +} + +.changeLabel { + font-family: var(--font-sans); + font-size: var(--type-label); + letter-spacing: var(--letter-spacing-label); + text-transform: uppercase; + color: var(--fg-3); +} + +.changeValue { + color: var(--fg-1); + font-variant-numeric: tabular-nums; +} + +.spark { + display: flex; + align-items: center; + justify-content: center; + padding: 8px; + color: var(--fg-2); +} diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/ETHPanel.tsx b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/ETHPanel.tsx new file mode 100644 index 00000000..0f1718c2 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/pages/panels/ETHPanel.tsx @@ -0,0 +1,122 @@ +// ©AngelaMos | 2026 +// ETHPanel.tsx + +import { useEffect } from 'react' +import { useSnapshot } from '@/api/snapshot' +import { usePrices } from '@/stores/prices' +import styles from './ETHPanel.module.scss' +import { Panel } from './Panel' +import { Sparkline } from './shared/Sparkline' + +const SYMBOL = 'ETH-USD' +const SPARKLINE_WIDTH = 280 +const SPARKLINE_HEIGHT = 22 +const PERCENT_DECIMALS = 2 +const PRICE_DECIMALS = 2 +const STALE_AFTER_MS = 60_000 + +interface CoinbaseSnapshotTick { + symbol: string + ts: string + price: string + volume_24h?: string +} + +export function ETHPanel(): React.ReactElement { + const { data } = useSnapshot() + const latest = usePrices((s) => s.latest[SYMBOL]) + const history = usePrices((s) => s.history[SYMBOL]) + const pushTick = usePrices((s) => s.pushTick) + + const seed = data?.coinbase_price as CoinbaseSnapshotTick | undefined + useEffect(() => { + if (!seed || seed.symbol !== SYMBOL) return + pushTick({ + symbol: seed.symbol, + ts: new Date(seed.ts).getTime(), + price: seed.price, + volume24h: seed.volume_24h, + }) + }, [seed, pushTick]) + + const priceNum = latest ? Number(latest.price) : null + const closes = (history ?? []).map((b) => Number(b.close)) + const pct1h = computeChangePct(closes) + + const lastTickAt = latest?.ts + const isStale = + latest === undefined ? undefined : Date.now() - latest.ts > STALE_AFTER_MS + + return ( + +
+ {fmtPrice(priceNum)} + USD +
+
+ +
+
+ +
+
+ ) +} + +ETHPanel.displayName = 'ETHPanel' + +function ChangeRow({ + label, + pct, +}: { + label: string + pct: number | null +}): React.ReactElement { + return ( +
+ {label} + {fmtPct(pct)} +
+ ) +} + +function computeChangePct(closes: number[]): number | null { + if (closes.length < 2) return null + const first = closes[0] + const last = closes[closes.length - 1] + if ( + first === undefined || + last === undefined || + !Number.isFinite(first) || + !Number.isFinite(last) || + first === 0 + ) { + return null + } + return ((last - first) / first) * 100 +} + +function fmtPct(pct: number | null): string { + if (pct === null) return '—' + const glyph = pct >= 0 ? '△' : '▽' + return `${glyph} ${Math.abs(pct).toFixed(PERCENT_DECIMALS)} %` +} + +function fmtPrice(n: number | null): string { + if (n === null || !Number.isFinite(n)) return '—' + return n.toLocaleString(undefined, { + minimumFractionDigits: PRICE_DECIMALS, + maximumFractionDigits: PRICE_DECIMALS, + }) +}