From f781687b5a11ecd2706f47828024ff5478a7f7d7 Mon Sep 17 00:00:00 2001 From: Prithvish Baidya Date: Sun, 6 Sep 2026 08:24:06 +0530 Subject: [PATCH] web: expand a signup row to show its custom fields and metadata The signups table only had room for email, status, source and date, so anything collected through custom fields was invisible in the admin. Each row now toggles open (click the row or the chevron) to a details panel: the waitlist's custom fields in schema order with their labels, values submitted under keys no longer in the schema, and the submission metadata (timestamps, source, IP, user agent). Expanded state is keyed by signup id so it survives paging. The actions menu stops propagation so it no longer toggles the row. Co-Authored-By: Claude Fable 5.1 --- .../routes/admin/waitlists/[id]/signups.tsx | 111 ++++++++++++++++-- 1 file changed, 104 insertions(+), 7 deletions(-) diff --git a/apps/web/src/routes/admin/waitlists/[id]/signups.tsx b/apps/web/src/routes/admin/waitlists/[id]/signups.tsx index 71cbbf1..2a5522d 100644 --- a/apps/web/src/routes/admin/waitlists/[id]/signups.tsx +++ b/apps/web/src/routes/admin/waitlists/[id]/signups.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { Fragment, useState } from "react"; import { useParams } from "react-router-dom"; import { toast } from "sonner"; import { HugeiconsIcon } from "@hugeicons/react"; @@ -8,6 +8,7 @@ import { MoreHorizontalIcon, ArrowLeft01Icon, ArrowRight01Icon, + ArrowDown01Icon, UserGroupIcon, CheckmarkCircle02Icon, Clock01Icon, @@ -63,6 +64,8 @@ import { getExportUrl, ApiError, type SignupsParams, + type Signup, + type CustomField, } from "@/lib/queries"; import { useDelayedLoading } from "@/lib/hooks"; import { cn, formatNumber, formatRelativeTime } from "@/lib/utils"; @@ -76,6 +79,15 @@ export function WaitlistSignupsPage() { }); const [searchInput, setSearchInput] = useState(""); const [showCharts, setShowCharts] = useState(true); + // Rows expanded to show custom fields + metadata. Keyed by signup id so paging keeps them. + const [expanded, setExpanded] = useState>(new Set()); + const toggleExpanded = (signupId: string) => + setExpanded((prev) => { + const next = new Set(prev); + if (next.has(signupId)) next.delete(signupId); + else next.add(signupId); + return next; + }); const { data: waitlistData } = useWaitlist(id!); const { data: signupsData, isLoading, isError: signupsError, error: signupsErrorData } = useSignups(id!, params); @@ -311,6 +323,7 @@ export function WaitlistSignupsPage() { + # Email Status @@ -323,6 +336,7 @@ export function WaitlistSignupsPage() { {showSignupsLoading && !signupsData ? ( Array.from({ length: 10 }).map((_, i) => ( + @@ -333,7 +347,7 @@ export function WaitlistSignupsPage() { )) ) : signupsError ? ( - +
@@ -345,7 +359,7 @@ export function WaitlistSignupsPage() { ) : !isLoading && signups.length === 0 ? ( - +

No signups yet

@@ -356,8 +370,27 @@ export function WaitlistSignupsPage() { ) : ( - signups.map((signup) => ( - + signups.map((signup) => { + const isOpen = expanded.has(signup.id); + return ( + + toggleExpanded(signup.id)} + > + + + {signup.position} @@ -371,7 +404,7 @@ export function WaitlistSignupsPage() { {formatRelativeTime(signup.createdAt)} - + e.stopPropagation()}>
@@ -444,6 +486,61 @@ export function WaitlistSignupsPage() { ); } +// Everything the table has no room for: the waitlist's custom fields (in the order they were +// defined, with their labels), then the submission metadata. +function SignupDetails({ signup, fields }: { signup: Signup; fields: CustomField[] }) { + const data = signup.customData || {}; + const known = fields.map((f) => ({ key: f.key, label: f.label, value: data[f.key] })); + const knownKeys = new Set(fields.map((f) => f.key)); + // Values submitted under a key that is no longer in the schema still deserve to be seen. + const extra = Object.entries(data) + .filter(([k]) => !knownKeys.has(k)) + .map(([key, value]) => ({ key, label: key, value })); + const custom = [...known, ...extra]; + + const meta: { label: string; value: string | null | undefined; mono?: boolean }[] = [ + { label: "Signed up", value: new Date(signup.createdAt).toLocaleString() }, + { label: "Confirmed", value: signup.confirmedAt ? new Date(signup.confirmedAt).toLocaleString() : "Not confirmed" }, + { label: "Unsubscribed", value: signup.unsubscribedAt ? new Date(signup.unsubscribedAt).toLocaleString() : null }, + { label: "Source", value: signup.referralSource || "direct" }, + { label: "IP address", value: signup.ipAddress, mono: true }, + { label: "User agent", value: signup.userAgent, mono: true }, + ]; + + return ( +
+
+

Custom fields

+ {custom.length === 0 ? ( +

This waitlist has no custom fields.

+ ) : ( +
+ {custom.map((f) => ( + +
{f.label}
+
+ {f.value || "—"} +
+
+ ))} +
+ )} +
+
+

Details

+
+ {meta.filter((m) => m.value).map((m) => ( + +
{m.label}
+
{m.value}
+
+ ))} +
+
+
+ ); +} + function StatusBadge({ status }: { status: "pending" | "confirmed" | "invited" }) { return (