Live picks: 'Live stats · N orders' title, X icon in the phone popup

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
simandClaude Sonnet 5.5 committed 2026-11-06 14:06:40 +01:00
1 parent 269fe03a80
commit 2665d3e6e0
2 files changed
+14 -14

No files matched your search

+7 -7
View File
@@ -52,14 +52,14 @@ 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: /ordered/ });
await expect(banner).toContainText("1 ordered · Veggie leads (1)");
const banner = page.getByRole("button", { name: /stats/ });
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();
// polling picks up another participant without a reload
await submit((await signIn()).userId, order.id, { burger: "Classic" });
await expect(banner).toContainText("2 ordered", { timeout: 10_000 });
await expect(banner).toContainText("Live stats · 2 orders", { timeout: 10_000 });
await banner.click();
await expect(page.getByText("2 people have ordered")).toBeVisible();
@@ -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: 2 ordered");
await expect(banner).toContainText("Final stats · 2 orders");
});
test("live picks on phones: bolt button with count opens a popup and minimizes again", async ({ browser, user }) => {
@@ -79,13 +79,13 @@ test("live picks on phones: bolt button with count opens a popup and minimizes a
await loginAs(page, first);
await page.goto(`/order/${order.id}`);
const fab = page.getByRole("button", { name: "Show live picks, 1 ordered" });
const fab = page.getByRole("button", { name: "Show live stats, 1 order" });
await expect(fab).toBeVisible();
await fab.click();
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: /1 ordered/ }).click();
await page.getByRole("button", { name: /stats · 1 order/ }).click();
await expect(page.getByText("1 person has ordered")).toBeHidden();
await expect(fab).toBeVisible();
await page.close();
@@ -99,7 +99,7 @@ test("live picks on phones: closed order gets the grey final button", async ({ b
const page = await browser.newPage({ viewport: { width: 390, height: 800 } });
await loginAs(page, first);
await page.goto(`/order/${order.id}`);
await expect(page.getByRole("button", { name: "Show live picks, 1 ordered" })).toHaveClass(/closed/);
await expect(page.getByRole("button", { name: "Show live stats, 1 order" })).toHaveClass(/closed/);
await page.close();
});
@@ -1,5 +1,5 @@
import React, { useState } from "react";
import { DownOutlined, LockOutlined, ThunderboltFilled } from "@ant-design/icons";
import { CloseOutlined, DownOutlined, LockOutlined, ThunderboltFilled } from "@ant-design/icons";
import { Card, Flex, Progress, Typography } from "antd";
import type { OrderStats } from "../../lib/services/ordersService";
import { getStoredValue, setStoredValue } from "../../lib/storage";
@@ -49,20 +49,20 @@ function Bars({ stats }: { stats: OrderStats }) {
}
function summary(stats: OrderStats, closed: boolean) {
const top = stats.categories[0]?.items[0];
return `${closed ? "Final: " : ""}${stats.participant_count} ordered${top ? ` · ${top.name} ${closed ? "led" : "leads"} (${top.count})` : ""}`;
const n = stats.participant_count;
return `${closed ? "Final" : "Live"} stats · ${n} ${n === 1 ? "order" : "orders"}`;
}
const icon = (closed: boolean) => (closed ? <LockOutlined /> : <ThunderboltFilled />);
/** One card for both layouts: the animated banner is its header, the bars unfold below it. */
function LivePicksCard({ stats, closed, open, onToggle }: { stats: OrderStats; closed: boolean; open: boolean; onToggle: () => void }) {
function LivePicksCard({ stats, closed, open, onToggle, dismiss }: { stats: OrderStats; closed: boolean; open: boolean; onToggle: () => void; dismiss?: boolean }) {
return (
<div className={open ? "live-card open" : "live-card"}>
<button className={closed ? "live-banner closed" : "live-banner"} aria-expanded={open} onClick={onToggle}>
{icon(closed)}
<span className="live-banner-text">{summary(stats, closed)}</span>
<DownOutlined className={open ? "live-chevron open" : "live-chevron"} />
{dismiss ? <CloseOutlined /> : <DownOutlined className={open ? "live-chevron open" : "live-chevron"} />}
</button>
{open && (
<Card size="small" className="live-card-body" styles={{ body: { padding: "12px 16px" } }}>
@@ -84,11 +84,11 @@ export function LivePicksFab({ stats, closed }: { stats: OrderStats; closed: boo
const [open, setOpen] = useStoredFlag(EXPANDED_KEY);
return (
<div className="live-fab-wrap">
{open && <LivePicksCard stats={stats} closed={closed} open onToggle={() => setOpen(false)} />}
{open && <LivePicksCard stats={stats} closed={closed} open dismiss onToggle={() => setOpen(false)} />}
{!open && (
<button
className={closed ? "live-fab closed" : "live-fab"}
aria-label={`Show live picks, ${stats.participant_count} ordered`}
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>