Closed style for live picks, remember expanded state, creator email in home orders list

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
simandClaude Sonnet 5.5 committed 2026-11-06 13:57:03 +01:00
1 parent f4f708e3ff
commit 496acfd87a
7 files changed
+58 -26

No files matched your search

+1
View File
@@ -167,6 +167,7 @@ def row_to_my_order(row: sqlite3.Row) -> dict:
"image_url": row["image_url"],
"closed": bool(row["closed"]),
"created_at": row["created_at"],
"creator_email": row["creator_email"],
"is_owner": bool(row["admin_token"]),
"is_participant": bool(row["submission_token"]),
"submission": {
+2 -1
View File
@@ -35,7 +35,7 @@ from .services.account_service import (
upsert_user_profile_email,
)
from .services.common_service import clean_image_url, clean_user_email, clean_user_id, ensure_email_not_privileged, is_site_admin, now_iso
from .services.stats_service import get_managed_stats, get_my_stats, get_order_stats, get_site_stats
from .services.stats_service import OWNER_EMAIL_SQL, get_managed_stats, get_my_stats, get_order_stats, get_site_stats
from .services.backup_service import start_backup_scheduler
from .services.config_service import get_email_settings, load_config
from .services.email_service import (
@@ -904,6 +904,7 @@ def get_my_orders(
go.closed,
go.created_at,
go.admin_token AS order_admin_token,
{OWNER_EMAIL_SQL} AS creator_email,
uot.admin_token,
uot.submission_token,
s.choices_json,
+25 -1
View File
@@ -52,7 +52,7 @@ test("live picks: counts, podium stamp, hide/show, frozen after close", async ({
await loginAs(page, first);
await page.goto(`/order/${order.id}`);
const card = page.locator(".ant-card", { hasText: "Live picks" }).filter({ hasText: "ordered" });
const card = page.locator(".ant-card", { hasText: /(Live|Final) picks/ }).filter({ hasText: "ordered" });
await expect(card.getByText("1 person has ordered")).toBeVisible();
await expect(card.getByText("Veggie")).toBeVisible();
await expect(page.getByText("You were 1st to order!")).toBeVisible();
@@ -88,3 +88,27 @@ test("live picks on phones: banner opens a sheet, dismiss leaves a bolt", async
await expect(page.getByRole("status")).toBeVisible();
await page.close();
});
test("live picks on phones: closed order gets the final banner, expanded state survives reload", async ({ browser, user }) => {
const order = await createOrder(user.userId, { title: "Closed picks" });
const first = await signIn();
await submit(first.userId, order.id, { burger: "Veggie" });
await api(`/api/orders/${order.id}/admin/status`, { userId: user.userId, method: "PUT", body: { closed: true } });
const page = await browser.newPage({ viewport: { width: 390, height: 800 } });
await loginAs(page, first);
await page.goto(`/order/${order.id}`);
const banner = page.getByRole("status");
await expect(banner).toContainText("Final: 1 ordered · Veggie led (1)");
await expect(banner).toHaveClass(/closed/);
await page.getByRole("button", { name: /Final:/ }).click();
await page.reload();
await expect(page.getByText("1 person has ordered")).toBeVisible();
await page.close();
});
test("home list shows creator email, state and date", async ({ userPage, user }) => {
await createOrder(user.userId, { title: "Creator on home" });
await userPage.goto("/");
await expect(userPage.getByText(new RegExp(`Open • ${user.email} • `))).toBeVisible();
});
@@ -121,6 +121,7 @@ export default function HomeOrdersTable({
{closedStr}
</Tooltip>
}
{item.creator_email ? ` • ${item.creator_email}` : ""}
{" • "}
{
<Tooltip title={`Created at: ${dateStr}`}>
+21 -18
View File
@@ -1,5 +1,5 @@
import React, { useState } from "react";
import { CloseOutlined, ThunderboltFilled } from "@ant-design/icons";
import { CloseOutlined, LockOutlined, ThunderboltFilled } from "@ant-design/icons";
import { Button, Card, Flex, Progress, Typography } from "antd";
import type { OrderStats } from "../../lib/services/ordersService";
import { getStoredValue, setStoredValue } from "../../lib/storage";
@@ -7,13 +7,15 @@ import { getStoredValue, setStoredValue } from "../../lib/storage";
const { Text } = Typography;
const HIDDEN_KEY = "lunchtime_live_picks_hidden";
function useHidden() {
const [hidden, setHidden] = useState(() => getStoredValue(HIDDEN_KEY) === "1");
const EXPANDED_KEY = "lunchtime_live_picks_expanded";
function useStoredFlag(key: string) {
const [flag, setFlag] = useState(() => getStoredValue(key) === "1");
return [
hidden,
flag,
(value: boolean) => {
setStoredValue(HIDDEN_KEY, value ? "1" : "0");
setHidden(value);
setStoredValue(key, value ? "1" : "0");
setFlag(value);
},
] as const;
}
@@ -48,8 +50,8 @@ function Bars({ stats }: { stats: OrderStats }) {
}
/** Desktop: quiet side panel that can be tucked away. */
export function LivePicksPanel({ stats }: { stats: OrderStats }) {
const [hidden, setHidden] = useHidden();
export function LivePicksPanel({ stats, closed }: { stats: OrderStats; closed: boolean }) {
const [hidden, setHidden] = useStoredFlag(HIDDEN_KEY);
if (hidden) {
return (
<Button type="text" size="small" icon={<ThunderboltFilled />} onClick={() => setHidden(false)}>
@@ -62,8 +64,8 @@ export function LivePicksPanel({ stats }: { stats: OrderStats }) {
size="small"
title={
<Flex align="center" gap={8}>
<span className="live-dot" aria-hidden />
Live picks
<span className={closed ? "live-dot closed" : "live-dot"} aria-hidden />
{closed ? "Final picks" : "Live picks"}
</Flex>
}
extra={
@@ -78,15 +80,15 @@ export function LivePicksPanel({ stats }: { stats: OrderStats }) {
}
/** Phones: floating energy banner that opens a sheet with the bars; dismiss leaves a small bolt to bring it back. */
export function LivePicksBanner({ stats }: { stats: OrderStats }) {
const [hidden, setHidden] = useHidden();
const [open, setOpen] = useState(false);
export function LivePicksBanner({ stats, closed }: { stats: OrderStats; closed: boolean }) {
const [hidden, setHidden] = useStoredFlag(HIDDEN_KEY);
const [open, setOpen] = useStoredFlag(EXPANDED_KEY);
const top = stats.categories[0]?.items[0];
if (hidden) {
return (
<button className="live-fab" aria-label="Show live picks" onClick={() => setHidden(false)}>
<ThunderboltFilled />
<button className={closed ? "live-fab closed" : "live-fab"} aria-label="Show live picks" onClick={() => setHidden(false)}>
{closed ? <LockOutlined /> : <ThunderboltFilled />}
</button>
);
}
@@ -97,11 +99,12 @@ export function LivePicksBanner({ stats }: { stats: OrderStats }) {
<Bars stats={stats} />
</Card>
)}
<div className="live-banner" role="status">
<div className={closed ? "live-banner closed" : "live-banner"} role="status">
<button className="live-banner-main" aria-expanded={open} onClick={() => setOpen(!open)}>
<ThunderboltFilled />
{closed ? <LockOutlined /> : <ThunderboltFilled />}
<span>
{stats.participant_count} ordered{top ? ` · ${top.name} leads (${top.count})` : ""}
{closed ? "Final: " : ""}
{stats.participant_count} ordered{top ? ` · ${top.name} ${closed ? "led" : "leads"} (${top.count})` : ""}
</span>
</button>
<button className="live-banner-close" aria-label="Hide live picks" onClick={() => setHidden(true)}>
+4
View File
@@ -229,6 +229,10 @@
background: #ea580c;
box-shadow: 0 6px 18px rgba(194, 65, 12, 0.45);
}
.live-dot.closed { background: #8c8c8c; animation: none; }
.live-banner.closed { background: #6b6560; animation: none; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); }
.live-banner.closed .anticon { animation: none; }
.live-fab.closed { background: #6b6560; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); }
@media (prefers-reduced-motion: reduce) {
.live-dot, .live-banner, .live-banner-main .anticon { animation: none; }
}
+4 -6
View File
@@ -346,9 +346,6 @@ export default function ParticipantView({ orderId }: { orderId: string }) {
// Live pick counts; polls only while open, keeps the last result (frozen) once closed.
const [stats, setStats] = useState<OrderStats | null>(null);
useEffect(() => {
if (isClosed) {
return;
}
let mounted = true;
const refreshStats = () =>
apiService.orders
@@ -356,7 +353,8 @@ export default function ParticipantView({ orderId }: { orderId: string }) {
.then((latest) => mounted && setStats(latest))
.catch(() => {});
refreshStats();
const intervalId = setInterval(refreshStats, 5000);
// Closed orders are fetched once (final numbers), open ones keep polling.
const intervalId = isClosed ? undefined : setInterval(refreshStats, 5000);
return () => {
mounted = false;
clearInterval(intervalId);
@@ -629,11 +627,11 @@ export default function ParticipantView({ orderId }: { orderId: string }) {
</Card>
{showPanel && (
<aside className="live-picks-aside">
<LivePicksPanel stats={stats} />
<LivePicksPanel stats={stats} closed={isClosed} />
</aside>
)}
</div>
{showBanner && <LivePicksBanner stats={stats} />}
{showBanner && <LivePicksBanner stats={stats} closed={isClosed} />}
</Space>
</div>