Add /crush33 comp-ticket portal + ticket-type badge on scan
Some checks failed
Build Android APK / build-apk (push) Failing after 1h11m4s

Portal (/crush33): password-gated page (PORTAL_PASSWORD) for admins to create
entry-only tickets from just name + email, with a category
(Guest/Worker/Performer/Volunteer/Speaker). Creates a 1-admission ticket, emails
the QR, and shows the QR on-screen. New Ticket Type column.

Scanner: shows a prominent TYPE badge (🎭 PERFORMER, 🛠️ WORKER, …) on the
confirm + success screens and in admin, so staff can see it's a special ticket.
Added Worker + Performer personas to /test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hank 2026-07-13 03:30:08 +00:00
parent b7c77afe02
commit 4b78c82b35
9 changed files with 230 additions and 0 deletions

View file

@ -202,6 +202,7 @@ function TicketCard({ ticket, onAdjust }: { ticket: TicketView; onAdjust: (t: Ti
const tags: string[] = [];
const e = ticket.extras;
if (ticket.ticketType) tags.push(`🎫 ${ticket.ticketType}`);
if (e.donorTier === "member") tags.push("🐻 Member");
else if (e.isDonor) tags.push("⭐ Donor");
if (e.carParking) tags.push("🚗 Car");

View file

@ -242,6 +242,7 @@ export default function ScannerScreen() {
{phase === "success" && ticket && (
<ResultOverlay status="success" onDismiss={resume}>
<Text style={styles.bigIcon}></Text>
<TypeBadge type={ticket.ticketType} />
{isIce ? (
<>
<Text style={styles.bigTitle}>
@ -366,6 +367,25 @@ function ExtrasRow({ ticket }: { ticket: TicketView }) {
);
}
const TYPE_ICON: Record<string, string> = {
Guest: "🎫",
Worker: "🛠️",
Performer: "🎭",
Volunteer: "🙌",
Speaker: "🎤",
};
function TypeBadge({ type }: { type: string }) {
if (!type) return null;
return (
<View style={styles.typeBadge}>
<Text style={styles.typeBadgeText}>
{(TYPE_ICON[type] ?? "🎫") + " " + type.toUpperCase()}
</Text>
</View>
);
}
function AdultNames({ names }: { names: string[] }) {
if (!names.length) return null;
return (
@ -405,6 +425,7 @@ function ConfirmCard({
return (
<ScrollView style={styles.card} contentContainerStyle={styles.cardContent}>
<Text style={styles.cardName}>{ticket.name}</Text>
<TypeBadge type={ticket.ticketType} />
<Text style={styles.cardCode}>{ticket.code}</Text>
<Text style={styles.cardCounts}>
<Text style={{ color: theme.successBright, fontWeight: "800" }}>{remaining}</Text> of {total} {unit} remaining
@ -507,6 +528,14 @@ const styles = StyleSheet.create({
donorFigureDivider: { width: 1, alignSelf: "stretch", backgroundColor: "rgba(255,255,255,0.35)", marginVertical: 8 },
donorEmail: { color: "rgba(255,255,255,0.85)", fontSize: 14, marginTop: 18 },
typeBadge: {
backgroundColor: "rgba(255,255,255,0.22)",
borderRadius: 999,
paddingHorizontal: 18,
paddingVertical: 8,
marginTop: 10,
},
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" },
tags: { flexDirection: "row", flexWrap: "wrap", justifyContent: "center", gap: 8, marginTop: 14 },

View file

@ -21,6 +21,7 @@ export interface TicketView {
code: string;
name: string;
email: string;
ticketType: string;
total: number;
redeemed: number;
remaining: number;