diff --git a/site/src/components/CommunityView.svelte b/site/src/components/CommunityView.svelte index 1fffc4d..4351c81 100644 --- a/site/src/components/CommunityView.svelte +++ b/site/src/components/CommunityView.svelte @@ -6,6 +6,7 @@ export let base: string = '/'; type Period = 'week' | 'month' | 'year' | 'all'; + type SortKey = 'display_name' | 'count' | 'distance_m' | 'elevation_m' | 'duration_s' | 'sports' | 'streak'; interface UserRaw { handle: string; @@ -33,6 +34,8 @@ } let period: Period = 'month'; + let sortKey: SortKey = 'distance_m'; + let sortAsc = false; let users: UserRaw[] = []; let stats: UserStat[] = []; let totals: Totals = { count: 0, distance_m: 0, elevation_m: 0, duration_s: 0, users: 0 }; @@ -46,7 +49,6 @@ if (p === 'all') return new Date(0); if (p === 'year') return new Date(now.getFullYear(), 0, 1); if (p === 'month') return new Date(now.getFullYear(), now.getMonth(), 1); - // week: Monday const d = new Date(now); const day = d.getDay(); d.setDate(d.getDate() - (day === 0 ? 6 : day - 1)); @@ -54,7 +56,6 @@ return d; } - /** Max consecutive active days across all public activities (all-time). */ function maxStreak(activities: ActivitySummary[]): number { if (!activities.length) return 0; const days = [...new Set(activities.map(a => a.started_at.slice(0, 10)))].sort(); @@ -75,7 +76,6 @@ for (const u of rawUsers) { const pub = u.activities.filter(a => a.privacy !== 'private'); const filtered = pub.filter(a => new Date(a.started_at) >= start); - if (filtered.length === 0) continue; const stat: UserStat = { handle: u.handle, @@ -117,24 +117,15 @@ try { const rootUrl = `${base}data/index.json`; const root: BASIndex = await fetch(rootUrl).then(r => r.json()); - const userShards = (root.shards ?? []).filter(s => s.handle); - if (userShards.length === 0) { - error = 'No community members found.'; - return; - } + if (userShards.length === 0) { error = 'No community members found.'; return; } const results = await Promise.allSettled( userShards.map(async shard => { - const shardBase = `${base}data/`; - const url = shard.url.startsWith('http') ? shard.url : `${shardBase}${shard.url}`; + const url = shard.url.startsWith('http') ? shard.url : `${base}data/${shard.url}`; const shardIndex: BASIndex = await fetch(url).then(r => r.json()); const activities = await fetchShard(url); - return { - handle: shard.handle!, - display_name: shardIndex.owner?.display_name ?? shard.handle!, - activities, - } as UserRaw; + return { handle: shard.handle!, display_name: shardIndex.owner?.display_name ?? shard.handle!, activities } as UserRaw; }) ); @@ -147,37 +138,30 @@ } } - $: if (users.length) { - ({ stats, totals } = computeStats(users, period)); + $: if (users.length) ({ stats, totals } = computeStats(users, period)); + + $: sorted = [...stats].sort((a, b) => { + let av: number | string, bv: number | string; + if (sortKey === 'display_name') { av = a.display_name.toLowerCase(); bv = b.display_name.toLowerCase(); } + else if (sortKey === 'sports') { av = a.sports.length; bv = b.sports.length; } + else { av = a[sortKey] as number; bv = b[sortKey] as number; } + if (av < bv) return sortAsc ? -1 : 1; + if (av > bv) return sortAsc ? 1 : -1; + return 0; + }); + + function setSort(key: SortKey) { + if (sortKey === key) sortAsc = !sortAsc; + else { sortKey = key; sortAsc = false; } + } + + function chevron(key: SortKey) { + if (sortKey !== key) return ''; + return sortAsc ? ' ↑' : ' ↓'; } onMount(loadData); - // ── Ranking helpers ─────────────────────────────────────────────────────── - - function top3(key: K, min = 0): UserStat[] { - return [...stats] - .filter(u => (u[key] as number) > min) - .sort((a, b) => (b[key] as number) - (a[key] as number)) - .slice(0, 3); - } - - function top3Sports(): UserStat[] { - return [...stats] - .filter(u => u.sports.length > 1) - .sort((a, b) => b.sports.length - a.sports.length) - .slice(0, 3); - } - - const MEDAL = ['🥇', '🥈', '🥉']; - - $: rowsCount = top3('count', 0); - $: rowsDistance = top3('distance_m', 0); - $: rowsElevation = top3('elevation_m', 0); - $: rowsDuration = top3('duration_s', 0); - $: rowsSports = top3Sports(); - $: rowsStreak = [...stats].filter(u => u.streak > 1).sort((a, b) => b.streak - a.streak).slice(0, 3); - const PERIODS: { key: Period; label: string }[] = [ { key: 'week', label: 'This week' }, { key: 'month', label: 'This month' }, @@ -186,8 +170,7 @@ ]; -
- +

Community

What everyone's been up to — together.

@@ -215,7 +198,7 @@ {/each}
- + {#if totals.users > 0}
{#each [ @@ -232,117 +215,78 @@
{/if} - + {#if totals.users === 0}

No public activities in this period yet.

{:else} -
- - - {#if rowsCount.length} -
-

Out there

-

Who kept showing up

- {#each rowsCount as u, i} -
-
- {MEDAL[i]} - {u.display_name} -
- {u.count} {u.count === 1 ? 'activity' : 'activities'} -
- {/each} -
- {/if} - - - {#if rowsDistance.length} -
-

Going far

-

Who covered the most ground

- {#each rowsDistance as u, i} -
-
- {MEDAL[i]} - {u.display_name} -
- {formatDistance(u.distance_m)} -
- {/each} -
- {/if} - - - {#if rowsElevation.length} -
-

Reaching new heights

-

Who climbed the most

- {#each rowsElevation as u, i} -
-
- {MEDAL[i]} - {u.display_name} -
- {Math.round(u.elevation_m).toLocaleString()} m -
- {/each} -
- {/if} - - - {#if rowsDuration.length} -
-

Hours on the move

-

Who invested the most time

- {#each rowsDuration as u, i} -
-
- {MEDAL[i]} - {u.display_name} -
- {formatDuration(u.duration_s)} -
- {/each} -
- {/if} - - - {#if rowsSports.length} -
-

Explorer

-

Who tried the most sports

- {#each rowsSports as u, i} -
-
- {MEDAL[i]} - {u.display_name} -
- +
+ + + + + + + + + + + + + + + {#each sorted as u, i} + + + + + + + + + + {/each} - - {/if} - - - {#if rowsStreak.length} -
-

Never stopped

-

Longest streak of consecutive days (all time)

- {#each rowsStreak as u, i} -
-
- {MEDAL[i]} - {u.display_name} -
- {u.streak} days -
- {/each} -
- {/if} - + +
# + + + + + + + + + +
{i + 1} + + {u.display_name} + + @{u.handle} + {u.count}{u.distance_m > 0 ? formatDistance(u.distance_m) : '—'}{u.elevation_m > 0 ? `${Math.round(u.elevation_m).toLocaleString()} m` : '—'}{u.duration_s > 0 ? formatDuration(u.duration_s) : '—'}
{/if} + {/if}