// Growth — new-member sign-ups, month over month.
//
// Wired to /api/v2/analytics/signups-mom. Source is contacts.became_member_at,
// stamped on the is_member false->true transition during GHL sync (membership
// is tag-derived in GHL). It is FORWARD-LOOKING: GHL has no historical signup
// date, so months before tracking began read 0. We surface `tracking_since`
// and say so explicitly rather than implying a real zero.
function monthLabel(ym) {
if (!ym) return '';
const d = new Date(ym + '-01T00:00:00');
return isNaN(d) ? ym : d.toLocaleString('en-US', { month: 'short', year: 'numeric' });
}
// Vertical MoM bar chart. Last bar is accented; zero/empty months render as a
// faint stub so the time axis stays honest about gaps.
function GrowthBars({ buckets, height = 280 }) {
if (!buckets || buckets.length === 0) {
return
New member sign-ups per month, so you can see change and growth.
{/* Inline-style the active state (not .btn.primary — that class is
reserved for primary CTAs and goes full-width on mobile). */}
{[6, 12, 24].map(m => (
))}
{res.error &&
Sign-ups data unavailable.
}
This month · new members
{latest ? latest.count : '—'}
{latest && deltaPct != null ? (
{deltaPct >= 0 ? '+' : ''}{deltaPct}% vs last month
({deltaAbs >= 0 ? '+' : ''}{deltaAbs})
) : (
no prior month to compare
)}
{/* Honesty banner: this metric only exists going forward. Shown loudly
while there's little/no history so nobody reads an empty chart as
"zero new members". */}
{!res.loading && tracked === 0 && (
No data yet. New member sign-ups come from the
GymMaster roster (Revive Gym) — members who joined in a month with a real membership.
If this is empty, the GymMaster sync hasn't completed yet; it refreshes a few times a
day. The chart fills in once the sync runs.