From 4dfffe7bc8e16f62f9f298871dc3db36f7ab5288 Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 6 Nov 2026 14:09:49 +0100 Subject: [PATCH] Live picks: closed state is 'Stats' with a chart icon and default card colors Co-Authored-By: Claude Sonnet 5.5 --- src/e2e/tests/orders.ui.spec.ts | 6 ++--- .../src/components/views/LivePicks.tsx | 22 ++++++++++++++----- src/frontend/src/styles.css | 5 ++--- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/e2e/tests/orders.ui.spec.ts b/src/e2e/tests/orders.ui.spec.ts index 6ddcccd..0610d82 100644 --- a/src/e2e/tests/orders.ui.spec.ts +++ b/src/e2e/tests/orders.ui.spec.ts @@ -52,7 +52,7 @@ test("live picks: counts, podium stamp, expand/collapse, frozen after close", as await loginAs(page, first); await page.goto(`/order/${order.id}`); - const banner = page.getByRole("button", { name: /stats/ }); + const banner = page.getByRole("button", { name: /stats/i }); await expect(banner).toContainText("Live stats · 1 order"); await expect(page.getByText("You were 1st to order!")).toBeVisible(); await expect(page.getByText("1 person has ordered")).toBeHidden(); @@ -68,7 +68,7 @@ test("live picks: counts, podium stamp, expand/collapse, frozen after close", as await api(`/api/orders/${order.id}/admin/status`, { userId: user.userId, method: "PUT", body: { closed: true } }); await expect(page.getByText(/This order is closed/)).toBeVisible({ timeout: 10_000 }); - await expect(banner).toContainText("Final stats · 2 orders"); + await expect(banner).toContainText("Stats · 2 orders"); }); test("live picks on phones: bolt button with count opens a popup and minimizes again", async ({ browser, user }) => { @@ -85,7 +85,7 @@ test("live picks on phones: bolt button with count opens a popup and minimizes a await expect(page.getByText("1 person has ordered")).toBeVisible(); await page.reload(); await expect(page.getByText("1 person has ordered")).toBeVisible(); // expanded state is remembered - await page.getByRole("button", { name: /stats · 1 order/ }).click(); + await page.getByRole("button", { name: /stats · 1 order/i }).click(); await expect(page.getByText("1 person has ordered")).toBeHidden(); await expect(fab).toBeVisible(); await page.close(); diff --git a/src/frontend/src/components/views/LivePicks.tsx b/src/frontend/src/components/views/LivePicks.tsx index 89fedc3..608710d 100644 --- a/src/frontend/src/components/views/LivePicks.tsx +++ b/src/frontend/src/components/views/LivePicks.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; -import { CloseOutlined, DownOutlined, LockOutlined, ThunderboltFilled } from "@ant-design/icons"; -import { Card, Flex, Progress, Typography } from "antd"; +import { BarChartOutlined, CloseOutlined, DownOutlined, ThunderboltFilled } from "@ant-design/icons"; +import { Card, Flex, Progress, theme, Typography } from "antd"; import type { OrderStats } from "../../lib/services/ordersService"; import { getStoredValue, setStoredValue } from "../../lib/storage"; @@ -50,16 +50,23 @@ function Bars({ stats }: { stats: OrderStats }) { function summary(stats: OrderStats, closed: boolean) { const n = stats.participant_count; - return `${closed ? "Final" : "Live"} stats · ${n} ${n === 1 ? "order" : "orders"}`; + return `${closed ? "Stats" : "Live stats"} · ${n} ${n === 1 ? "order" : "orders"}`; } -const icon = (closed: boolean) => (closed ? : ); +const icon = (closed: boolean) => (closed ? : ); + +/** Closed: no custom colors, just the default card look. */ +function useClosedStyle(closed: boolean): React.CSSProperties | undefined { + const { token } = theme.useToken(); + return closed ? { background: token.colorBgContainer, color: token.colorText, border: `1px solid ${token.colorBorderSecondary}` } : undefined; +} /** One card for both layouts: the animated banner is its header, the bars unfold below it. */ function LivePicksCard({ stats, closed, open, onToggle, dismiss }: { stats: OrderStats; closed: boolean; open: boolean; onToggle: () => void; dismiss?: boolean }) { + const closedStyle = useClosedStyle(closed); return (
- )}
diff --git a/src/frontend/src/styles.css b/src/frontend/src/styles.css index f8f7ff6..8f1c737 100644 --- a/src/frontend/src/styles.css +++ b/src/frontend/src/styles.css @@ -197,7 +197,7 @@ .live-chevron.open { transform: rotate(180deg); } @keyframes live-flow { to { background-position: -300% 0; } } @keyframes live-zap { 50% { transform: scale(1.15); filter: drop-shadow(0 0 3px rgba(255, 255, 255, 0.7)); } } -.live-banner.closed { background: #6b6560; animation: none; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); } +.live-banner.closed { animation: none; box-shadow: none; } .live-banner.closed > .anticon:first-child { animation: none; } .live-fab-wrap { position: fixed; @@ -227,7 +227,7 @@ box-shadow: 0 4px 12px rgba(180, 86, 42, 0.35); } .live-fab:not(.closed) .live-fab-icon { display: inline-block; animation: live-zap 1.6s ease-in-out infinite; } -.live-fab.closed { background: #6b6560; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); } +.live-fab.closed { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); } @media (prefers-reduced-motion: reduce) { .live-banner, .live-banner > .anticon:first-child, .live-fab-icon { animation: none !important; } } @@ -246,4 +246,3 @@ background: #fff; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35); } -.live-fab.closed .live-fab-count { color: #6b6560; }