mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-04-06 01:43:32 -04:00
added global route for stat tracking
This commit is contained in:
198
src/routes/global/+page.svelte
Normal file
198
src/routes/global/+page.svelte
Normal file
@@ -0,0 +1,198 @@
|
||||
<script lang="ts">
|
||||
import Container from '$lib/components/Container.svelte';
|
||||
|
||||
interface Stats {
|
||||
todayCount: number;
|
||||
totalCount: number;
|
||||
uniquePlayers: number;
|
||||
weeklyPlayers: number;
|
||||
activeStreaks: number;
|
||||
avgGuessesToday: number | null;
|
||||
registeredUsers: number;
|
||||
avgCompletionsPerPlayer: number | null;
|
||||
}
|
||||
|
||||
interface PageData {
|
||||
todayEst: string;
|
||||
stats: Stats;
|
||||
last14Days: { date: string; count: number }[];
|
||||
streakChart: { days: number; count: number }[];
|
||||
growth: {
|
||||
completionsVelocity: number;
|
||||
completionsAcceleration: number;
|
||||
userVelocity: number;
|
||||
userAcceleration: number;
|
||||
newUsers7d: number;
|
||||
churned7d: number;
|
||||
netGrowth7d: number;
|
||||
};
|
||||
}
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
const { stats, last14Days, todayEst, streakChart, growth } = $derived(data);
|
||||
|
||||
function signed(n: number, unit = ''): string {
|
||||
if (n > 0) return `+${n}${unit}`;
|
||||
if (n < 0) return `${n}${unit}`;
|
||||
return `0${unit}`;
|
||||
}
|
||||
|
||||
function trendColor(n: number): string {
|
||||
if (n > 0) return 'text-green-400';
|
||||
if (n < 0) return 'text-red-400';
|
||||
return 'text-gray-400';
|
||||
}
|
||||
|
||||
const maxCount = $derived(Math.max(1, ...last14Days.map((d) => d.count)));
|
||||
|
||||
const maxStreakCount = $derived(Math.max(1, ...streakChart.map((r) => r.count)));
|
||||
|
||||
const statCards = $derived([
|
||||
{ label: 'Completions Today', value: String(stats.todayCount) },
|
||||
{ label: 'All-Time Completions', value: String(stats.totalCount) },
|
||||
{ label: 'Unique Players', value: String(stats.uniquePlayers) },
|
||||
{ label: 'Players This Week', value: String(stats.weeklyPlayers) },
|
||||
{ label: 'Active Streaks', value: String(stats.activeStreaks) },
|
||||
{
|
||||
label: 'Avg Guesses Today',
|
||||
value: stats.avgGuessesToday != null ? stats.avgGuessesToday.toFixed(2) : 'N/A',
|
||||
},
|
||||
{ label: 'Registered Users', value: String(stats.registeredUsers) },
|
||||
{
|
||||
label: 'Avg Completions/Player',
|
||||
value: stats.avgCompletionsPerPlayer != null ? stats.avgCompletionsPerPlayer.toFixed(2) : 'N/A',
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Global Stats | Bibdle</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="min-h-screen bg-linear-to-br from-gray-900 via-slate-900 to-gray-900 text-gray-100">
|
||||
<div class="max-w-6xl mx-auto px-4 py-8">
|
||||
|
||||
<a href="/" class="inline-flex items-center gap-1 text-gray-400 hover:text-gray-100 text-sm mb-6 transition-colors">
|
||||
← Back to Game
|
||||
</a>
|
||||
|
||||
<header class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-100">Global Stats</h1>
|
||||
<p class="text-gray-400 text-sm mt-1">EST reference date: {todayEst}</p>
|
||||
</header>
|
||||
|
||||
<section class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4 mb-10">
|
||||
{#each statCards as card (card.label)}
|
||||
<Container class="w-full p-5 gap-2">
|
||||
<span class="text-gray-400 text-xs uppercase tracking-wide text-center">{card.label}</span>
|
||||
<span class="text-2xl md:text-3xl font-bold text-gray-100">{card.value}</span>
|
||||
</Container>
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
<section class="mb-10">
|
||||
<h2 class="text-lg font-semibold text-gray-100 mb-4">Traffic & Growth <span class="text-xs font-normal text-gray-400">(7-day windows)</span></h2>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
<Container class="w-full p-5 gap-2">
|
||||
<span class="text-gray-400 text-xs uppercase tracking-wide text-center">Completions Velocity</span>
|
||||
<span class="text-2xl md:text-3xl font-bold text-center {trendColor(growth.completionsVelocity)}">{signed(growth.completionsVelocity, '/day')}</span>
|
||||
<span class="text-xs text-gray-500 text-center">vs prior 7 days</span>
|
||||
</Container>
|
||||
<Container class="w-full p-5 gap-2">
|
||||
<span class="text-gray-400 text-xs uppercase tracking-wide text-center">Completions Accel.</span>
|
||||
<span class="text-2xl md:text-3xl font-bold text-center {trendColor(growth.completionsAcceleration)}">{signed(growth.completionsAcceleration, '/day')}</span>
|
||||
<span class="text-xs text-gray-500 text-center">rate of change of velocity</span>
|
||||
</Container>
|
||||
<Container class="w-full p-5 gap-2">
|
||||
<span class="text-gray-400 text-xs uppercase tracking-wide text-center">User Velocity</span>
|
||||
<span class="text-2xl md:text-3xl font-bold text-center {trendColor(growth.userVelocity)}">{signed(growth.userVelocity)}</span>
|
||||
<span class="text-xs text-gray-500 text-center">unique players, wk/wk</span>
|
||||
</Container>
|
||||
<Container class="w-full p-5 gap-2">
|
||||
<span class="text-gray-400 text-xs uppercase tracking-wide text-center">User Acceleration</span>
|
||||
<span class="text-2xl md:text-3xl font-bold text-center {trendColor(growth.userAcceleration)}">{signed(growth.userAcceleration)}</span>
|
||||
<span class="text-xs text-gray-500 text-center">rate of change of user velocity</span>
|
||||
</Container>
|
||||
<Container class="w-full p-5 gap-2">
|
||||
<span class="text-gray-400 text-xs uppercase tracking-wide text-center">New Players (7d)</span>
|
||||
<span class="text-2xl md:text-3xl font-bold text-center {trendColor(growth.newUsers7d)}">{String(growth.newUsers7d)}</span>
|
||||
<span class="text-xs text-gray-500 text-center">first-time players</span>
|
||||
</Container>
|
||||
<Container class="w-full p-5 gap-2">
|
||||
<span class="text-gray-400 text-xs uppercase tracking-wide text-center">Churned (7d)</span>
|
||||
<span class="text-2xl md:text-3xl font-bold text-center {trendColor(0)}">{String(growth.churned7d)}</span>
|
||||
<span class="text-xs text-gray-500 text-center">played wk prior, not this wk</span>
|
||||
</Container>
|
||||
<Container class="w-full p-5 gap-2">
|
||||
<span class="text-gray-400 text-xs uppercase tracking-wide text-center">Net Growth (7d)</span>
|
||||
<span class="text-2xl md:text-3xl font-bold text-center {trendColor(growth.netGrowth7d)}">{signed(growth.netGrowth7d)}</span>
|
||||
<span class="text-xs text-gray-500 text-center">new minus churned</span>
|
||||
</Container>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-8">
|
||||
<h2 class="text-lg font-semibold text-gray-100 mb-4">Last 14 Days</h2>
|
||||
<div class="overflow-x-auto rounded-xl border border-white/10">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="bg-white/5 text-gray-400 text-xs uppercase tracking-wide">
|
||||
<th class="text-left px-4 py-3">Date</th>
|
||||
<th class="text-right px-4 py-3">Completions</th>
|
||||
<th class="px-4 py-3 w-48"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each last14Days as row (row.date)}
|
||||
{@const barPct = Math.round((row.count / maxCount) * 100)}
|
||||
<tr class="border-t border-white/5 hover:bg-white/5 transition-colors">
|
||||
<td class="px-4 py-3 text-gray-300">{row.date}</td>
|
||||
<td class="px-4 py-3 text-right text-gray-100 font-medium">{row.count}</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="w-full min-w-24">
|
||||
<div class="bg-amber-500 h-4 rounded" style="width: {barPct}%"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-8">
|
||||
<h2 class="text-lg font-semibold text-gray-100 mb-4">Active Streak Distribution</h2>
|
||||
{#if streakChart.length === 0}
|
||||
<p class="text-gray-400 text-sm px-4 py-6">No active streaks yet.</p>
|
||||
{:else}
|
||||
<div class="overflow-x-auto rounded-xl border border-white/10">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="bg-white/5 text-gray-400 text-xs uppercase tracking-wide">
|
||||
<th class="text-left px-4 py-3">Days</th>
|
||||
<th class="text-right px-4 py-3">Players</th>
|
||||
<th class="px-4 py-3 w-48"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each streakChart as row (row.days)}
|
||||
{@const barPct = Math.round((row.count / maxStreakCount) * 100)}
|
||||
<tr class="border-t border-white/5 hover:bg-white/5 transition-colors">
|
||||
<td class="px-4 py-3 text-gray-300">{row.days}</td>
|
||||
<td class="px-4 py-3 text-right text-gray-100 font-medium">{row.count}</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="w-full min-w-24">
|
||||
<div class="bg-blue-500 h-4 rounded" style="width: {barPct}%"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user