Live picks: closed state is 'Stats' with a chart icon and default card colors

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
simandClaude Sonnet 5.5 committed 2026-11-06 14:09:49 +01:00
1 parent 8da4142165
commit 4dfffe7bc8
3 files changed
+21 -12

No files matched your search

+3 -3
View File
@@ -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();
@@ -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 ? <LockOutlined /> : <ThunderboltFilled />);
const icon = (closed: boolean) => (closed ? <BarChartOutlined /> : <ThunderboltFilled />);
/** 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 (
<div className={open ? "live-card open" : "live-card"}>
<button className={closed ? "live-banner closed" : "live-banner"} aria-expanded={open} onClick={onToggle}>
<button className={closed ? "live-banner closed" : "live-banner"} style={closedStyle} aria-expanded={open} onClick={onToggle}>
{icon(closed)}
<span className="live-banner-text">{summary(stats, closed)}</span>
{dismiss ? <CloseOutlined /> : <DownOutlined className={open ? "live-chevron open" : "live-chevron"} />}
@@ -82,17 +89,20 @@ export function LivePicksBanner({ stats, closed }: { stats: OrderStats; closed:
/** Phones: a bolt button with the order count; tap for the card as a popup, tap its header to minimize. */
export function LivePicksFab({ stats, closed }: { stats: OrderStats; closed: boolean }) {
const [open, setOpen] = useStoredFlag(EXPANDED_KEY);
const closedStyle = useClosedStyle(closed);
const { token } = theme.useToken();
return (
<div className="live-fab-wrap">
{open && <LivePicksCard stats={stats} closed={closed} open dismiss onToggle={() => setOpen(false)} />}
{!open && (
<button
className={closed ? "live-fab closed" : "live-fab"}
style={closedStyle}
aria-label={`Show live stats, ${stats.participant_count} ${stats.participant_count === 1 ? "order" : "orders"}`}
onClick={() => setOpen(true)}
>
<span className="live-fab-icon">{icon(closed)}</span>
<span className="live-fab-count">{stats.participant_count}</span>
<span className="live-fab-count" style={closed ? { background: token.colorText, color: token.colorBgContainer } : undefined}>{stats.participant_count}</span>
</button>
)}
</div>
+2 -3
View File
@@ -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; }