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 loginAs(page, first);
await page.goto(`/order/${order.id}`); await page.goto(`/order/${order.id}`);
const banner = page.getByRole("button", { name: /ordered/ }); const banner = page.getByRole("button", { name: /stats/ });
await expect(banner).toContainText("1 ordered · Veggie leads (1)"); await expect(banner).toContainText("Live stats · 1 order");
await expect(page.getByText("You were 1st to order!")).toBeVisible(); await expect(page.getByText("You were 1st to order!")).toBeVisible();
await expect(page.getByText("1 person has ordered")).toBeHidden(); await expect(page.getByText("1 person has ordered")).toBeHidden();
// polling picks up another participant without a reload // polling picks up another participant without a reload
await submit((await signIn()).userId, order.id, { burger: "Classic" }); 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 banner.click();
await expect(page.getByText("2 people have ordered")).toBeVisible(); 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 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(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 }) => { 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 loginAs(page, first);
await page.goto(`/order/${order.id}`); 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 expect(fab).toBeVisible();
await fab.click(); await fab.click();
await expect(page.getByText("1 person has ordered")).toBeVisible(); await expect(page.getByText("1 person has ordered")).toBeVisible();
await page.reload(); await page.reload();
await expect(page.getByText("1 person has ordered")).toBeVisible(); // expanded state is remembered 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(page.getByText("1 person has ordered")).toBeHidden();
await expect(fab).toBeVisible(); await expect(fab).toBeVisible();
await page.close(); 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 } }); const page = await browser.newPage({ viewport: { width: 390, height: 800 } });
await loginAs(page, first); await loginAs(page, first);
await page.goto(`/order/${order.id}`); 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(); await page.close();
}); });
@@ -1,5 +1,5 @@
import React, { useState } from "react"; 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 { Card, Flex, Progress, Typography } from "antd";
import type { OrderStats } from "../../lib/services/ordersService"; import type { OrderStats } from "../../lib/services/ordersService";
import { getStoredValue, setStoredValue } from "../../lib/storage"; import { getStoredValue, setStoredValue } from "../../lib/storage";
@@ -49,20 +49,20 @@ function Bars({ stats }: { stats: OrderStats }) {
} }
function summary(stats: OrderStats, closed: boolean) { function summary(stats: OrderStats, closed: boolean) {
const top = stats.categories[0]?.items[0]; const n = stats.participant_count;
return `${closed ? "Final: " : ""}${stats.participant_count} ordered${top ? ` · ${top.name} ${closed ? "led" : "leads"} (${top.count})` : ""}`; return `${closed ? "Final" : "Live"} stats · ${n} ${n === 1 ? "order" : "orders"}`;
} }
const icon = (closed: boolean) => (closed ? <LockOutlined /> : <ThunderboltFilled />); const icon = (closed: boolean) => (closed ? <LockOutlined /> : <ThunderboltFilled />);
/** One card for both layouts: the animated banner is its header, the bars unfold below it. */ /** 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 ( return (
<div className={open ? "live-card open" : "live-card"}> <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"} aria-expanded={open} onClick={onToggle}>
{icon(closed)} {icon(closed)}
<span className="live-banner-text">{summary(stats, closed)}</span> <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> </button>
{open && ( {open && (
<Card size="small" className="live-card-body" styles={{ body: { padding: "12px 16px" } }}> <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); const [open, setOpen] = useStoredFlag(EXPANDED_KEY);
return ( return (
<div className="live-fab-wrap"> <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 && ( {!open && (
<button <button
className={closed ? "live-fab closed" : "live-fab"} 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)} onClick={() => setOpen(true)}
> >
<span className="live-fab-icon">{icon(closed)}</span> <span className="live-fab-icon">{icon(closed)}</span>