From 4a5aef5a3d842df84b982de75f156e8a31456350 Mon Sep 17 00:00:00 2001 From: George Powell Date: Mon, 23 Mar 2026 20:17:30 -0400 Subject: [PATCH] refactor: extract CollapsibleTable component and fix show more Replaces 7 inline collapsible tables in the global stats page with a reusable CollapsibleTable component. Adds mode tab toggle (Rolling 30d / Calendar) into the component. Fixes show more/less which was broken due to mode-based expanded tracking when no modes were provided. Co-Authored-By: Claude Sonnet 4.6 --- src/lib/components/CollapsibleTable.svelte | 102 +++ src/routes/global/+page.svelte | 690 +++++++-------------- 2 files changed, 319 insertions(+), 473 deletions(-) create mode 100644 src/lib/components/CollapsibleTable.svelte diff --git a/src/lib/components/CollapsibleTable.svelte b/src/lib/components/CollapsibleTable.svelte new file mode 100644 index 0000000..4cebf87 --- /dev/null +++ b/src/lib/components/CollapsibleTable.svelte @@ -0,0 +1,102 @@ + + +{#if modes && modes.length > 1} +
+ {#each modes as m (m.value)} + {@const active = mode === m.value} + + {/each} +
+{/if} + +{#if rows.length === 0} + {#if empty} + {@render empty()} + {:else} +

Not enough data yet.

+ {/if} +{:else} +
+ + + + {#each headers as header (header.label)} + + {/each} + + + + {#each displayedRows as item, i (i)} + + {@render rowSnippet(item)} + + {/each} + +
+ {header.label} +
+
+ + {#if rows.length > initialRows} + + {/if} +{/if} diff --git a/src/routes/global/+page.svelte b/src/routes/global/+page.svelte index 727d5af..2ec9ba6 100644 --- a/src/routes/global/+page.svelte +++ b/src/routes/global/+page.svelte @@ -1,5 +1,6 @@ @@ -362,74 +358,34 @@ - {#if newPlayerReturnSeries.length > 0} -
- - - - - - - - - - - - {#each (returnExpanded ? newPlayerReturnSeries : newPlayerReturnSeries.slice(0, 3)) as row (row.date)} - - - - - - - - {/each} - -
DateNew PlayersReturn Rate7d Avg
{row.date}{row.cohort}{row.rate != null - ? `${row.rate}%` - : "—"}{row.rollingAvg != null - ? `${row.rollingAvg}%` - : "—"} -
- {#if row.rollingAvg != null} -
- {/if} -
-
-
- {#if newPlayerReturnSeries.length > 3} - - {/if} - {:else} -

- Not enough data yet. -

- {/if} + + {#snippet row(item)} + {item.date} + {item.cohort} + + {item.rate != null ? `${item.rate}%` : '—'} + + + {item.rollingAvg != null ? `${item.rollingAvg}%` : '—'} + + +
+ {#if item.rollingAvg != null} +
+ {/if} +
+ + {/snippet} +
@@ -440,184 +396,113 @@ Unique players per 7-day window. Most recent week first. Avg WAU: {avgWau}

-
- - - - - - - - - - - {#each (wauExpanded ? wauWeeks : wauWeeks.slice(0, 3)) as row (row.weekEnd)} - {@const barPct = Math.round( - (row.wau / maxWau) * 100, - )} - - - - - - - {/each} - -
WeekActive UsersWk/Wk Change
{row.weekStart} – {row.weekEnd}{row.wau} - {row.changePct != null - ? signed(row.changePct, "%") - : "—"} - -
-
-
-
-
- {#if wauWeeks.length > 3} - - {/if} + + {#snippet row(item)} + {@const barPct = Math.round((item.wau / maxWau) * 100)} + {item.weekStart} – {item.weekEnd} + {item.wau} + + {item.changePct != null ? signed(item.changePct, '%') : '—'} + + +
+
+
+ + {/snippet} +
-
-

Monthly Active Users

-
- - -
-
+

Monthly Active Users

- {mauMode === 'rolling' ? 'Unique players per 30-day window. Most recent first.' : 'Unique players per calendar month. Current month projected to end of month.'} + {mauMode === 'rolling' + ? 'Unique players per 30-day window. Most recent first.' + : 'Unique players per calendar month. Current month projected to end of month.'}

{#if mauMode === 'rolling'} - {@const displayedMauMonths = mauExpanded ? mauMonths : mauMonths.slice(0, 3)} - {@const maxMau = Math.max(1, ...mauMonths.map((m) => m.mau))} -
- - - - - - - - - - - {#each displayedMauMonths as row (row.monthEnd)} - {@const barPct = Math.round((row.mau / maxMau) * 100)} - - - - - - - {/each} - -
PeriodActive UsersMo/Mo Change
{row.monthStart} – {row.monthEnd}{row.mau} - {row.changePct != null ? signed(row.changePct, '%') : '—'} - -
-
-
-
-
- {#if mauMonths.length > 3} - - {/if} + + {#snippet row(item)} + {@const barPct = Math.round((item.mau / maxMau) * 100)} + {item.monthStart} – {item.monthEnd} + {item.mau} + + {item.changePct != null ? signed(item.changePct, '%') : '—'} + + +
+
+
+ + {/snippet} +
{:else} - {@const displayedCalMau = mauExpanded ? calendarMauMonths : calendarMauMonths.slice(0, 3)} - {@const maxCalMau = Math.max(1, ...calendarMauMonths.map((m) => m.projectedMau ?? m.mau))} -
- - - - - - - - - - - {#each displayedCalMau as row (row.monthStart)} - {@const displayMau = row.projectedMau ?? row.mau} - {@const barPct = Math.round((displayMau / maxCalMau) * 100)} - - - - - - - {/each} - -
MonthActive UsersMo/Mo Change
- {row.label} - {#if row.isCurrentMonth} - (projected) - {/if} - - {#if row.isCurrentMonth} - {row.mau} → {row.projectedMau ?? row.mau} - {:else} - {row.mau} - {/if} - - {#if row.changePct != null} - {row.isCurrentMonth ? '~' : ''}{signed(row.changePct, '%')} - {:else} - — - {/if} - -
-
-
-
-
- {#if calendarMauMonths.length > 3} - - {/if} + + {#snippet row(item)} + {@const displayMau = item.projectedMau ?? item.mau} + {@const barPct = Math.round((displayMau / maxCalMau) * 100)} + + {item.label} + {#if item.isCurrentMonth} + (projected) + {/if} + + + {#if item.isCurrentMonth} + {item.mau} → {item.projectedMau ?? item.mau} + {:else} + {item.mau} + {/if} + + + {#if item.changePct != null} + {item.isCurrentMonth ? '~' : ''}{signed(item.changePct, '%')} + {:else} + — + {/if} + + +
+
+
+ + {/snippet} +
{/if}
@@ -625,112 +510,53 @@

Last 14 Days — Completions

-
- - - - - - - - - - {#each (completionsExpanded ? last14Days : last14Days.slice(0, 3)) as row (row.date)} - {@const barPct = Math.round( - (row.count / maxCount) * 100, - )} - - - - - - {/each} - -
DateCompletions
{row.date}{row.count} -
-
-
-
-
- {#if last14Days.length > 3} - - {/if} + + {#snippet row(item)} + {@const barPct = Math.round((item.count / maxCount) * 100)} + {item.date} + {item.count} + +
+
+
+ + {/snippet} +

Active Streak Distribution

- {#if streakChart.length === 0} -

- No active streaks yet. -

- {:else} -
- - - - - - - - - - {#each (streakExpanded ? streakChart : streakChart.slice(0, 3)) as row (row.days)} - {@const barPct = Math.round( - (row.count / maxStreakCount) * 100, - )} - - - - - - {/each} - -
DaysPlayers
{row.days}{row.count} -
-
-
-
-
- {#if streakChart.length > 3} - - {/if} - {/if} + + {#snippet row(item)} + {@const barPct = Math.round((item.count / maxStreakCount) * 100)} + {item.days} + {item.count} + +
+
+
+ + {/snippet} + {#snippet empty()} +

No active streaks yet.

+ {/snippet} +
@@ -747,67 +573,26 @@

7-Day Retention

- {#if retention7dSeries.length === 0} -

- Not enough data yet. -

- {:else} -
- - - - - - - - - - - {#each (ret7dExpanded ? retention7dSeries : retention7dSeries.slice(0, 3)) as row (row.date)} - - - - - - - {/each} - -
Cohort DatenRet. %
{row.date}{row.cohortSize}{row.rate}% -
-
-
-
-
- {#if retention7dSeries.length > 3} - - {/if} - {/if} + + {#snippet row(item)} + {item.date} + {item.cohortSize} + {item.rate}% + +
+
+
+ + {/snippet} +
@@ -815,67 +600,26 @@

30-Day Retention

- {#if retention30dSeries.length === 0} -

- Not enough data yet. -

- {:else} -
- - - - - - - - - - - {#each (ret30dExpanded ? retention30dSeries : retention30dSeries.slice(0, 3)) as row (row.date)} - - - - - - - {/each} - -
Cohort DatenRet. %
{row.date}{row.cohortSize}{row.rate}% -
-
-
-
-
- {#if retention30dSeries.length > 3} - - {/if} - {/if} + + {#snippet row(item)} + {item.date} + {item.cohortSize} + {item.rate}% + +
+
+
+ + {/snippet} +