mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-08-08 06:35:19 +02:00
feat: add measurement system support across ActivityCard, StravaActivityCard, NewLocationModal, LocationVisits, and related utility functions
This commit is contained in:
parent
31630de4fd
commit
65f99c5a50
6 changed files with 79 additions and 16 deletions
|
@ -6,11 +6,14 @@
|
||||||
import FileIcon from '~icons/mdi/file';
|
import FileIcon from '~icons/mdi/file';
|
||||||
import TrashIcon from '~icons/mdi/trash-can';
|
import TrashIcon from '~icons/mdi/trash-can';
|
||||||
import { formatDateInTimezone } from '$lib/dateUtils';
|
import { formatDateInTimezone } from '$lib/dateUtils';
|
||||||
|
import { getDistance, getElevation } from '$lib';
|
||||||
|
|
||||||
export let activity: Activity;
|
export let activity: Activity;
|
||||||
export let trails: Trail[];
|
export let trails: Trail[];
|
||||||
export let visit: Visit | TransportationVisit;
|
export let visit: Visit | TransportationVisit;
|
||||||
|
|
||||||
|
export let measurementSystem: 'metric' | 'imperial' = 'metric';
|
||||||
|
|
||||||
export let readOnly: boolean = false;
|
export let readOnly: boolean = false;
|
||||||
|
|
||||||
$: trail = activity.trail ? trails.find((t) => t.id === activity.trail) : null;
|
$: trail = activity.trail ? trails.find((t) => t.id === activity.trail) : null;
|
||||||
|
@ -35,7 +38,10 @@
|
||||||
<div class="text-xs text-base-content/70 space-y-1">
|
<div class="text-xs text-base-content/70 space-y-1">
|
||||||
{#if activity.distance}
|
{#if activity.distance}
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<span>Distance: {activity.distance} km</span>
|
<span
|
||||||
|
>Distance: {getDistance(measurementSystem, activity.distance)}
|
||||||
|
{measurementSystem === 'imperial' ? 'miles' : 'km'}</span
|
||||||
|
>
|
||||||
{#if activity.moving_time}
|
{#if activity.moving_time}
|
||||||
<span>Time: {activity.moving_time}</span>
|
<span>Time: {activity.moving_time}</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -45,10 +51,16 @@
|
||||||
{#if activity.elevation_gain || activity.elevation_loss}
|
{#if activity.elevation_gain || activity.elevation_loss}
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
{#if activity.elevation_gain}
|
{#if activity.elevation_gain}
|
||||||
<span>↗ {activity.elevation_gain}m</span>
|
<span
|
||||||
|
>↗ {getElevation(measurementSystem, activity.elevation_gain)}
|
||||||
|
{measurementSystem === 'imperial' ? 'feet' : 'm'}</span
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
{#if activity.elevation_loss}
|
{#if activity.elevation_loss}
|
||||||
<span>↘ {activity.elevation_loss}m</span>
|
<span
|
||||||
|
>↘ {getElevation(measurementSystem, activity.elevation_loss)}
|
||||||
|
{measurementSystem === 'imperial' ? 'feet' : 'm'}</span
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -294,6 +294,7 @@
|
||||||
steps[2].selected = true;
|
steps[2].selected = true;
|
||||||
}}
|
}}
|
||||||
on:close={() => close()}
|
on:close={() => close()}
|
||||||
|
measurementSystem={user?.measurement_system || 'metric'}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let activity: StravaActivity;
|
export let activity: StravaActivity;
|
||||||
|
export let measurementSystem: 'metric' | 'imperial' = 'metric';
|
||||||
|
|
||||||
interface SportConfig {
|
interface SportConfig {
|
||||||
color: string;
|
color: string;
|
||||||
|
@ -48,10 +49,21 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatPace(seconds: number): string {
|
function formatPace(seconds: number, system: 'metric' | 'imperial'): string {
|
||||||
const minutes = Math.floor(seconds / 60);
|
const minutes = Math.floor(seconds / 60);
|
||||||
const secs = Math.floor(seconds % 60);
|
const secs = Math.floor(seconds % 60);
|
||||||
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
const unit = system === 'metric' ? 'km' : 'mi';
|
||||||
|
return `${minutes}:${secs.toString().padStart(2, '0')}/${unit}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertElevation(
|
||||||
|
meters: number,
|
||||||
|
system: 'metric' | 'imperial'
|
||||||
|
): { value: number; unit: string } {
|
||||||
|
if (system === 'imperial') {
|
||||||
|
return { value: meters * 3.28084, unit: 'ft' };
|
||||||
|
}
|
||||||
|
return { value: meters, unit: 'm' };
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleImportActivity() {
|
function handleImportActivity() {
|
||||||
|
@ -59,6 +71,21 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
$: typeConfig = getTypeConfig(activity.sport_type);
|
$: typeConfig = getTypeConfig(activity.sport_type);
|
||||||
|
$: distance =
|
||||||
|
measurementSystem === 'metric'
|
||||||
|
? { value: activity.distance_km, unit: 'km' }
|
||||||
|
: { value: activity.distance_miles, unit: 'mi' };
|
||||||
|
$: speed =
|
||||||
|
measurementSystem === 'metric'
|
||||||
|
? { value: activity.average_speed_kmh, unit: 'km/h' }
|
||||||
|
: { value: activity.average_speed_mph, unit: 'mph' };
|
||||||
|
$: maxSpeed =
|
||||||
|
measurementSystem === 'metric'
|
||||||
|
? { value: activity.max_speed_kmh, unit: 'km/h' }
|
||||||
|
: { value: activity.max_speed_mph, unit: 'mph' };
|
||||||
|
$: elevation = convertElevation(activity.total_elevation_gain, measurementSystem);
|
||||||
|
$: paceSeconds =
|
||||||
|
measurementSystem === 'metric' ? activity.pace_per_km_seconds : activity.pace_per_mile_seconds;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="card bg-base-50 border border-base-200 hover:shadow-md transition-shadow">
|
<div class="card bg-base-50 border border-base-200 hover:shadow-md transition-shadow">
|
||||||
|
@ -147,8 +174,8 @@
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
|
||||||
<div class="stat bg-base-100 rounded-lg p-3">
|
<div class="stat bg-base-100 rounded-lg p-3">
|
||||||
<div class="stat-title text-xs">Distance</div>
|
<div class="stat-title text-xs">Distance</div>
|
||||||
<div class="stat-value text-lg">{activity.distance_km.toFixed(2)}</div>
|
<div class="stat-value text-lg">{distance.value.toFixed(2)}</div>
|
||||||
<div class="stat-desc">km ({activity.distance_miles.toFixed(2)} mi)</div>
|
<div class="stat-desc">{distance.unit}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat bg-base-100 rounded-lg p-3">
|
<div class="stat bg-base-100 rounded-lg p-3">
|
||||||
<div class="stat-title text-xs">Time</div>
|
<div class="stat-title text-xs">Time</div>
|
||||||
|
@ -157,13 +184,13 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="stat bg-base-100 rounded-lg p-3">
|
<div class="stat bg-base-100 rounded-lg p-3">
|
||||||
<div class="stat-title text-xs">Avg Speed</div>
|
<div class="stat-title text-xs">Avg Speed</div>
|
||||||
<div class="stat-value text-lg">{activity.average_speed_kmh.toFixed(1)}</div>
|
<div class="stat-value text-lg">{speed.value.toFixed(1)}</div>
|
||||||
<div class="stat-desc">km/h ({activity.average_speed_mph.toFixed(1)} mph)</div>
|
<div class="stat-desc">{speed.unit}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat bg-base-100 rounded-lg p-3">
|
<div class="stat bg-base-100 rounded-lg p-3">
|
||||||
<div class="stat-title text-xs">Elevation</div>
|
<div class="stat-title text-xs">Elevation</div>
|
||||||
<div class="stat-value text-lg">{activity.total_elevation_gain.toFixed(0)}</div>
|
<div class="stat-value text-lg">{elevation.value.toFixed(0)}</div>
|
||||||
<div class="stat-desc">m gain</div>
|
<div class="stat-desc">{elevation.unit} gain</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -197,15 +224,16 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Footer with pace and max speed -->
|
<!-- Footer with pace and max speed -->
|
||||||
{#if activity.pace_per_km_seconds}
|
{#if paceSeconds}
|
||||||
<div class="flex justify-between items-center mt-3 pt-3 border-t border-base-300">
|
<div class="flex justify-between items-center mt-3 pt-3 border-t border-base-300">
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<span class="font-medium">Pace:</span>
|
<span class="font-medium">Pace:</span>
|
||||||
{formatPace(activity.pace_per_km_seconds)}/km
|
{formatPace(paceSeconds, measurementSystem)}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<span class="font-medium">Max Speed:</span>
|
<span class="font-medium">Max Speed:</span>
|
||||||
{activity.max_speed_kmh.toFixed(1)} km/h
|
{maxSpeed.value.toFixed(1)}
|
||||||
|
{maxSpeed.unit}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -45,6 +45,7 @@
|
||||||
export let visits: (Visit | TransportationVisit)[] | null = null;
|
export let visits: (Visit | TransportationVisit)[] | null = null;
|
||||||
export let objectId: string;
|
export let objectId: string;
|
||||||
export let trails: Trail[] = [];
|
export let trails: Trail[] = [];
|
||||||
|
export let measurementSystem: 'metric' | 'imperial' = 'metric';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
@ -627,7 +628,7 @@
|
||||||
name: stravaActivity.name,
|
name: stravaActivity.name,
|
||||||
type: stravaActivity.type,
|
type: stravaActivity.type,
|
||||||
sport_type: stravaActivity.sport_type || stravaActivity.type,
|
sport_type: stravaActivity.sport_type || stravaActivity.type,
|
||||||
distance: stravaActivity.distance ? stravaActivity.distance / 1000 : null, // Convert to km
|
distance: stravaActivity.distance || null, // Keep in meters
|
||||||
moving_time: stravaActivity.moving_time ? formatDuration(stravaActivity.moving_time) : '',
|
moving_time: stravaActivity.moving_time ? formatDuration(stravaActivity.moving_time) : '',
|
||||||
elapsed_time: stravaActivity.elapsed_time
|
elapsed_time: stravaActivity.elapsed_time
|
||||||
? formatDuration(stravaActivity.elapsed_time)
|
? formatDuration(stravaActivity.elapsed_time)
|
||||||
|
@ -1175,7 +1176,6 @@
|
||||||
class="input input-bordered input-sm w-full mt-1"
|
class="input input-bordered input-sm w-full mt-1"
|
||||||
placeholder="Morning Run"
|
placeholder="Morning Run"
|
||||||
bind:value={activityForm.name}
|
bind:value={activityForm.name}
|
||||||
readonly={!!pendingStravaImport[visit.id]}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1588,6 +1588,7 @@
|
||||||
{activity}
|
{activity}
|
||||||
{trails}
|
{trails}
|
||||||
{visit}
|
{visit}
|
||||||
|
{measurementSystem}
|
||||||
on:delete={(event) =>
|
on:delete={(event) =>
|
||||||
deleteActivity(event.detail.visitId, event.detail.activityId)}
|
deleteActivity(event.detail.visitId, event.detail.activityId)}
|
||||||
/>
|
/>
|
||||||
|
@ -1619,6 +1620,7 @@
|
||||||
<StravaActivityCard
|
<StravaActivityCard
|
||||||
{activity}
|
{activity}
|
||||||
on:import={(event) => handleStravaActivityImport(event, visit.id)}
|
on:import={(event) => handleStravaActivityImport(event, visit.id)}
|
||||||
|
{measurementSystem}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
@ -623,3 +623,22 @@ export function getBasemapUrl() {
|
||||||
}
|
}
|
||||||
return 'https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json';
|
return 'https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getDistance(measurementSystem: 'metric' | 'imperial', meters: number): string {
|
||||||
|
if (measurementSystem === 'imperial') {
|
||||||
|
const miles = meters / 1609.34;
|
||||||
|
return miles.toFixed(2);
|
||||||
|
} else {
|
||||||
|
const km = meters / 1000;
|
||||||
|
return km.toFixed(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getElevation(measurementSystem: 'metric' | 'imperial', elevation: number): string {
|
||||||
|
if (measurementSystem === 'imperial') {
|
||||||
|
const feet = elevation * 3.28084;
|
||||||
|
return Math.round(feet).toString();
|
||||||
|
} else {
|
||||||
|
return Math.round(elevation).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -613,6 +613,7 @@
|
||||||
readOnly={true}
|
readOnly={true}
|
||||||
trails={adventure.trails}
|
trails={adventure.trails}
|
||||||
{visit}
|
{visit}
|
||||||
|
measurementSystem={data.user?.measurement_system || 'metric'}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue