From 63e00fae618851640bdbfd35116e02b484879dd8 Mon Sep 17 00:00:00 2001 From: Hank Date: Mon, 20 Jul 2026 18:48:10 +0000 Subject: [PATCH] Scanner: large Adults / Kids party panel on scan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gate-enforcement aid against adults signing up under a free/cheaper kid bracket. When a ticket is scanned, a prominent amber-bordered panel shows big "# ADULTS # KIDS" counts (Adults = the 18+ bracket; Kids = youth 13-16 + all under-13), plus a per-bracket detail line (e.g. "1× 13-16 · 2× 5-9") so staff can eyeball the claimed ages against the actual party. Shown on the confirm card (before check-in) and the success overlay; hidden in Ice mode. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/app/index.tsx | 50 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/app/app/index.tsx b/app/app/index.tsx index b7a0b2e..3766c6c 100644 --- a/app/app/index.tsx +++ b/app/app/index.tsx @@ -253,6 +253,7 @@ export default function ScannerScreen() { {ticket.redeemed} of {ticket.total} redeemed · {ticket.remaining} remaining + @@ -394,6 +395,37 @@ function AdultNames({ names }: { names: string[] }) { ); } +/** + * Big Adults / Kids breakdown so gate staff can eyeball the party against the + * ticket — a deterrent for adults signing up under a (free/cheaper) kid bracket. + * Adults = the 18+ bracket; Kids = everyone else (youth 13-16 + all under-13). + */ +function PartyPanel({ ticket }: { ticket: TicketView }) { + const adults = ticket.ages.find((a) => a.bracket === "Adults")?.count ?? 0; + const kidBrackets = ticket.ages.filter((a) => a.bracket !== "Adults"); + const kids = kidBrackets.reduce((s, a) => s + a.count, 0); + return ( + + + + {adults} + {adults === 1 ? "ADULT" : "ADULTS"} + + + + {kids} + {kids === 1 ? "KID" : "KIDS"} + + + {kidBrackets.length > 0 && ( + + {kidBrackets.map((a) => `${a.count}× ${a.bracket.replace(/^(Kids|Youth)\s*/, "")}`).join(" · ")} + + )} + + ); +} + function ConfirmCard({ ticket, isIce, @@ -422,6 +454,7 @@ function ConfirmCard({ {ticket.name} {ticket.code} + {!isIce && } {remaining} of {total} {unit} remaining @@ -535,6 +568,23 @@ const styles = StyleSheet.create({ typeBadgeText: { color: "#fff", fontSize: 20, fontWeight: "900", letterSpacing: 1 }, namesBox: { marginTop: 12, alignItems: "center", gap: 3 }, nameLine: { color: "#fff", fontSize: 18, fontWeight: "600", textAlign: "center" }, + party: { + alignSelf: "stretch", + backgroundColor: "#1d2a1f", + borderWidth: 2, + borderColor: theme.warn, + borderRadius: 16, + paddingVertical: 18, + paddingHorizontal: 12, + marginTop: 16, + marginBottom: 2, + }, + partyRow: { flexDirection: "row", alignItems: "center", justifyContent: "center" }, + partyCell: { flex: 1, alignItems: "center" }, + partyNum: { color: theme.text, fontSize: 60, fontWeight: "900", lineHeight: 64 }, + partyLbl: { color: theme.warn, fontSize: 15, fontWeight: "800", letterSpacing: 2, marginTop: 2 }, + partyDivider: { width: 2, alignSelf: "stretch", backgroundColor: theme.cardBorder, marginVertical: 6 }, + partyDetail: { color: theme.textDim, fontSize: 14, textAlign: "center", marginTop: 12, fontWeight: "600" }, tags: { flexDirection: "row", flexWrap: "wrap", justifyContent: "center", gap: 8, marginTop: 14 }, tag: { color: "#fff", backgroundColor: "rgba(255,255,255,0.18)", paddingHorizontal: 10, paddingVertical: 5, borderRadius: 999, fontSize: 13, overflow: "hidden" },