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:
1 parent
8da4142165
commit
4dfffe7bc8
3 files changed
+21
-12
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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; }
|
||||
Reference in new issue
Block a user