Add map view toggle to activity feed

Adds a List/Map toggle to the feed and @user profile pages. The map view
plots all filtered activities as sport-coloured tracks on a MapLibre map
with no extra requests (uses preview_coords already in memory). Clicking
a track or list row selects it: pans the map to fit, expands the list
item with key stats, and scrolls it into view.
This commit is contained in:
Davide Scaini
2026-05-22 11:47:47 +02:00
parent 7f2a751065
commit df025873c6
2 changed files with 228 additions and 1 deletions
+38 -1
View File
@@ -3,6 +3,7 @@
import type { ActivitySummary, BASIndex, Sport } from '../lib/types';
import { formatDistance, formatDuration, formatElevation, formatDate, isUnlisted, sportIcon, sportColor, sportLabel } from '../lib/format';
import { loadIndexPaged, loadShardActivities, loadCombinedFeed } from '../lib/dataloader';
import FeedMapView from './FeedMapView.svelte';
/** Render preview_coords as an SVG polyline path string. */
function trackPath(coords: [number, number][] | null, w: number, h: number): string {
@@ -50,6 +51,7 @@
let loading = true;
let loadingMore = false;
let error = '';
let viewMode: 'list' | 'map' = 'list';
let mounted = false;
let pendingShards: string[] = [];
/** Grand total from feed.json — shows instance-wide count even before all pages are loaded. */
@@ -247,7 +249,7 @@
bind:value={query}
class="flex-1 px-4 py-2 rounded-lg bg-zinc-900 border border-zinc-700 text-white placeholder-zinc-500 text-sm focus:outline-none focus:border-zinc-500 transition-colors"
/>
<div class="flex gap-2">
<div class="flex gap-2 items-center">
<div class="flex items-center gap-1.5 px-3 py-2 rounded-lg bg-zinc-900 border border-zinc-700 focus-within:border-zinc-500 transition-colors">
<span class="text-xs text-zinc-500 whitespace-nowrap select-none">From</span>
<input
@@ -269,6 +271,22 @@
class="bg-transparent text-white text-sm focus:outline-none [color-scheme:dark]"
/>
</div>
<div class="flex rounded-lg border border-zinc-700 overflow-hidden text-sm shrink-0">
<button
class="px-3 py-2 transition-colors"
class:bg-zinc-800={viewMode === 'list'}
class:text-white={viewMode === 'list'}
class:text-zinc-400={viewMode !== 'list'}
on:click={() => viewMode = 'list'}
>List</button>
<button
class="px-3 py-2 border-l border-zinc-700 transition-colors"
class:bg-zinc-800={viewMode === 'map'}
class:text-white={viewMode === 'map'}
class:text-zinc-400={viewMode !== 'map'}
on:click={() => viewMode = 'map'}
>Map</button>
</div>
</div>
</div>
@@ -324,6 +342,25 @@
</div>
{:else if error}
<p class="text-red-400 text-center py-12">Could not load activities: {error}</p>
{:else if viewMode === 'map'}
<FeedMapView activities={withSearch} base={base} />
{#if hasMore}
<div class="text-center mt-4">
<button
class="px-6 py-2 rounded-full border border-zinc-700 text-zinc-300 hover:border-zinc-500 hover:text-white disabled:opacity-40 transition-colors text-sm"
disabled={loadingMore}
on:click={loadMore}
>
{#if loadingMore}
Loading…
{:else if canShowMore}
Load more ({filtered.length - shown} remaining)
{:else}
Load older activities ({pendingShards.length} more {pendingShards.length === 1 ? 'period' : 'periods'})
{/if}
</button>
</div>
{/if}
{:else if withSearch.length === 0}
<p class="text-zinc-500 text-center py-12">
{#if loadingAllShards}Loading…{:else if query.trim()}No activities match "{query.trim()}".{:else}No activities found.{/if}