feat: show pace (min/km) for running, hiking, walking, other activities
Cycling keeps km/h; pace sports show e.g. "5:30 /km" in the feed card, activity stat panel (avg/max), and laps table.
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
import { marked } from 'marked';
|
||||
import DOMPurify from 'dompurify';
|
||||
import type { ActivitySummary, ActivityDetail, AthleteZones, Timeseries } from '../lib/types';
|
||||
import { formatDistance, formatDuration, formatElevation, formatSpeed, formatDate, formatTime, formatElapsed, sportIcon, sportLabel, sportColor } from '../lib/format';
|
||||
import { formatDistance, formatDuration, formatElevation, formatSpeed, formatPace, isPaceSport, formatDate, formatTime, formatElapsed, sportIcon, sportLabel, sportColor } from '../lib/format';
|
||||
import ActivityMap from './ActivityMap.svelte';
|
||||
import ActivityCharts from './ActivityCharts.svelte';
|
||||
import ActivityPowerCurve from './ActivityPowerCurve.svelte';
|
||||
@@ -256,8 +256,8 @@
|
||||
: null,
|
||||
],
|
||||
[
|
||||
stat('Avg speed', formatSpeed(activity.avg_speed_kmh), 'speed'),
|
||||
stat('Max speed', formatSpeed(activity.max_speed_kmh), 'speed'),
|
||||
stat(isPaceSport(activity.sport) ? 'Avg pace' : 'Avg speed', isPaceSport(activity.sport) ? formatPace(activity.avg_speed_kmh) : formatSpeed(activity.avg_speed_kmh), 'speed'),
|
||||
stat(isPaceSport(activity.sport) ? 'Max pace' : 'Max speed', isPaceSport(activity.sport) ? formatPace(activity.max_speed_kmh) : formatSpeed(activity.max_speed_kmh), 'speed'),
|
||||
],
|
||||
[
|
||||
stat('Avg HR', activity.avg_hr_bpm ? `${activity.avg_hr_bpm} bpm` : '—', 'heart_rate'),
|
||||
@@ -517,7 +517,7 @@
|
||||
<td class="px-4 py-2 text-zinc-400">#{lap.index + 1}</td>
|
||||
<td class="px-4 py-2 text-white">{formatDistance(lap.distance_m)}</td>
|
||||
<td class="px-4 py-2 text-white">{formatDuration(lap.duration_s)}</td>
|
||||
<td class="px-4 py-2 text-white">{formatSpeed(lap.avg_speed_kmh)}</td>
|
||||
<td class="px-4 py-2 text-white">{isPaceSport(activity.sport) ? formatPace(lap.avg_speed_kmh) : formatSpeed(lap.avg_speed_kmh)}</td>
|
||||
<td class="px-4 py-2 text-white">{lap.avg_hr_bpm ? `${lap.avg_hr_bpm} bpm` : '—'}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import type { ActivitySummary, BASIndex, Sport } from '../lib/types';
|
||||
import { formatDistance, formatDuration, formatElevation, formatDate, isUnlisted, sportIcon, sportColor, sportLabel } from '../lib/format';
|
||||
import { formatDistance, formatDuration, formatElevation, formatDate, formatSpeed, formatPace, isPaceSport, isUnlisted, sportIcon, sportColor, sportLabel } from '../lib/format';
|
||||
import { loadIndexPaged, loadShardActivities, loadCombinedFeed } from '../lib/dataloader';
|
||||
import FeedMapView from './FeedMapView.svelte';
|
||||
|
||||
@@ -434,7 +434,7 @@
|
||||
{#if a.avg_speed_kmh || a.avg_hr_bpm}
|
||||
<div class="flex gap-4 mt-3 pt-3 border-t border-zinc-800 text-xs text-zinc-400">
|
||||
{#if a.avg_speed_kmh}
|
||||
<span>⚡ {a.avg_speed_kmh.toFixed(1)} km/h</span>
|
||||
<span>⚡ {isPaceSport(a.sport) ? formatPace(a.avg_speed_kmh) : formatSpeed(a.avg_speed_kmh)}</span>
|
||||
{/if}
|
||||
{#if a.avg_hr_bpm}
|
||||
<span>♥ {a.avg_hr_bpm} bpm</span>
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
import type { Privacy, Sport } from './types';
|
||||
|
||||
const PACE_SPORTS: Set<Sport> = new Set(['running', 'hiking', 'walking', 'other']);
|
||||
|
||||
export function isPaceSport(sport: Sport): boolean {
|
||||
return PACE_SPORTS.has(sport);
|
||||
}
|
||||
|
||||
export function formatPace(kmh: number | null): string {
|
||||
if (kmh == null || kmh <= 0) return '—';
|
||||
const secsPerKm = 3600 / kmh;
|
||||
const m = Math.floor(secsPerKm / 60);
|
||||
const s = Math.round(secsPerKm % 60);
|
||||
return `${m}:${s.toString().padStart(2, '0')} /km`;
|
||||
}
|
||||
|
||||
/** True for "unlisted" activities (and the legacy "private" alias).
|
||||
* Use this everywhere instead of comparing against 'private' directly. */
|
||||
export function isUnlisted(privacy: Privacy | string | null | undefined): boolean {
|
||||
|
||||
Reference in New Issue
Block a user