1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-08-05 05:05:17 +02:00

Add StravaActivity and Activity types to types.ts

- Introduced StravaActivity type to represent detailed activity data from Strava.
- Added Activity type to encapsulate user activities, including optional trail and GPX file information.
- Updated Location type to include an array of activities associated with each visit.
This commit is contained in:
Sean Morley 2025-08-03 13:41:57 -04:00
parent c7ff8f4bc7
commit 5046bd49f7
13 changed files with 1626 additions and 255 deletions

View file

@ -285,6 +285,7 @@
{#if steps[3].selected}
<LocationVisits
bind:visits={location.visits}
bind:trails={location.trails}
objectId={location.id}
type="location"
on:back={() => {

View file

@ -0,0 +1,205 @@
<script lang="ts">
import type { StravaActivity } from '$lib/types';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
export let activity: StravaActivity;
interface SportConfig {
color: string;
icon: string;
name: string;
}
const sportTypeConfig: Record<string, SportConfig> = {
StandUpPaddling: { color: 'info', icon: '🏄', name: 'Stand Up Paddling' },
Run: { color: 'success', icon: '🏃', name: 'Running' },
Ride: { color: 'warning', icon: '🚴', name: 'Cycling' },
Swim: { color: 'primary', icon: '🏊', name: 'Swimming' },
Hike: { color: 'accent', icon: '🥾', name: 'Hiking' },
Walk: { color: 'neutral', icon: '🚶', name: 'Walking' },
default: { color: 'secondary', icon: '⚡', name: 'Activity' }
};
function getTypeConfig(type: string): SportConfig {
return sportTypeConfig[type] || sportTypeConfig.default;
}
function formatTime(seconds: number): string {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
if (hours > 0) {
return `${hours}h ${minutes}m ${secs}s`;
}
return `${minutes}m ${secs}s`;
}
function formatDate(dateString: string): string {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
function formatPace(seconds: number): string {
const minutes = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${minutes}:${secs.toString().padStart(2, '0')}`;
}
function handleImportActivity() {
dispatch('import', activity);
}
$: typeConfig = getTypeConfig(activity.sport_type);
</script>
<div class="card bg-base-50 border border-base-200 hover:shadow-md transition-shadow">
<div class="card-body p-4">
<!-- Activity Header -->
<div class="flex items-start justify-between mb-3">
<div class="flex items-center gap-3">
<div class="text-2xl" aria-label="Sport icon">{typeConfig.icon}</div>
<div>
<h3 class="font-semibold text-lg">{activity.name}</h3>
<div class="flex items-center gap-2 text-sm text-base-content/70">
<span class="badge badge-{typeConfig.color} badge-sm">{typeConfig.name}</span>
<span></span>
<span>{formatDate(activity.start_date)}</span>
</div>
</div>
</div>
<div class="dropdown dropdown-end">
<div
tabindex="0"
role="button"
class="btn btn-ghost btn-sm btn-circle"
aria-label="Activity options"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zM12 13a1 1 0 110-2 1 1 0 010 2zM12 20a1 1 0 110-2 1 1 0 010 2z"
/>
</svg>
</div>
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<ul
tabindex="0"
class="dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow"
>
<li>
<a href={activity.export_gpx} target="_blank" rel="noopener noreferrer"> Export GPX </a>
</li>
<li>
<a href={activity.export_original} target="_blank" rel="noopener noreferrer">
Export Original
</a>
</li>
<li>
<a
href="https://www.strava.com/activities/{activity.id}"
target="_blank"
rel="noopener noreferrer"
>
View on Strava
</a>
</li>
<!-- import button -->
<li>
<button type="button" on:click={handleImportActivity} class="text-primary"
>Import Activity</button
>
</li>
</ul>
</div>
</div>
<!-- Main Stats -->
<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-title text-xs">Distance</div>
<div class="stat-value text-lg">{activity.distance_km.toFixed(2)}</div>
<div class="stat-desc">km ({activity.distance_miles.toFixed(2)} mi)</div>
</div>
<div class="stat bg-base-100 rounded-lg p-3">
<div class="stat-title text-xs">Time</div>
<div class="stat-value text-lg">{formatTime(activity.moving_time)}</div>
<div class="stat-desc">Moving time</div>
</div>
<div class="stat bg-base-100 rounded-lg p-3">
<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-desc">km/h ({activity.average_speed_mph.toFixed(1)} mph)</div>
</div>
<div class="stat bg-base-100 rounded-lg p-3">
<div class="stat-title text-xs">Elevation</div>
<div class="stat-value text-lg">{activity.total_elevation_gain.toFixed(0)}</div>
<div class="stat-desc">m gain</div>
</div>
</div>
<!-- Additional Stats -->
<div class="flex flex-wrap gap-2 text-sm">
{#if activity.average_cadence}
<div class="badge badge-ghost">
<span class="font-medium">Cadence:</span>&nbsp;{activity.average_cadence.toFixed(1)}
</div>
{/if}
{#if activity.calories}
<div class="badge badge-ghost">
<span class="font-medium">Calories:</span>&nbsp;{activity.calories}
</div>
{/if}
{#if activity.kudos_count > 0}
<div class="badge badge-ghost">
<span class="font-medium">Kudos:</span>&nbsp;{activity.kudos_count}
</div>
{/if}
{#if activity.achievement_count > 0}
<div class="badge badge-success badge-outline">
<span class="font-medium">Achievements:</span>&nbsp;{activity.achievement_count}
</div>
{/if}
{#if activity.pr_count > 0}
<div class="badge badge-warning badge-outline">
<span class="font-medium">PRs:</span>&nbsp;{activity.pr_count}
</div>
{/if}
</div>
<!-- Footer with pace and max speed -->
{#if activity.pace_per_km_seconds}
<div class="flex justify-between items-center mt-3 pt-3 border-t border-base-300">
<div class="text-sm">
<span class="font-medium">Pace:</span>
{formatPace(activity.pace_per_km_seconds)}/km
</div>
<div class="text-sm">
<span class="font-medium">Max Speed:</span>
{activity.max_speed_kmh.toFixed(1)} km/h
</div>
</div>
{/if}
</div>
</div>
<style>
.stat {
min-height: auto;
}
.stat-value {
font-size: 1.25rem;
line-height: 1.75rem;
}
</style>

File diff suppressed because it is too large Load diff

View file

@ -36,6 +36,7 @@ export type Location = {
end_date: string;
timezone: string | null;
notes: string;
activities: Activity[]; // Array of activities associated with the visit
}[];
collections?: string[] | null;
latitude: number | null;
@ -328,3 +329,87 @@ export type Trail = {
wanderer_id?: string | null; // Optional ID for integration with Wanderer
provider: string; // Provider of the trail data, e.g., 'wanderer', 'external'
};
export type StravaActivity = {
id: number;
name: string;
type: string;
sport_type: string;
distance: number;
distance_km: number;
distance_miles: number;
moving_time: number;
elapsed_time: number;
rest_time: number;
total_elevation_gain: number;
estimated_elevation_loss: number;
elev_high: number;
elev_low: number;
total_elevation_range: number;
start_date: string; // ISO 8601 format
start_date_local: string; // ISO 8601 format
timezone: string;
timezone_raw: string;
average_speed: number;
average_speed_kmh: number;
average_speed_mph: number;
max_speed: number;
max_speed_kmh: number;
max_speed_mph: number;
pace_per_km_seconds: number;
pace_per_mile_seconds: number;
grade_adjusted_average_speed: number | null;
average_cadence: number | null;
average_watts: number | null;
max_watts: number | null;
kilojoules: number | null;
calories: number | null;
achievement_count: number;
kudos_count: number;
comment_count: number;
pr_count: number;
gear_id: string | null;
device_name: string | null;
trainer: boolean;
manual: boolean;
start_latlng: [number, number] | null; // [latitude, longitude]
end_latlng: [number, number] | null; // [latitude, longitude]
export_original: string; // URL
export_gpx: string; // URL
visibility: string;
photo_count: number;
has_heartrate: boolean;
flagged: boolean;
commute: boolean;
};
export type Activity = {
id: string;
user: string;
visit: string;
trail: Trail | null;
gpx_file: string | null;
name: string;
type: string;
sport_type: string | null;
distance: number | null;
moving_time: string | null; // ISO 8601 duration string
elapsed_time: string | null; // ISO 8601 duration string
rest_time: string | null; // ISO 8601 duration string
elevation_gain: number | null;
elevation_loss: number | null;
elev_high: number | null;
elev_low: number | null;
start_date: string | null; // ISO 8601 date string
start_date_local: string | null; // ISO 8601 date string
timezone: string | null;
average_speed: number | null;
max_speed: number | null;
average_cadence: number | null;
calories: number | null;
start_lat: number | null;
start_lng: number | null;
end_lat: number | null;
end_lng: number | null;
external_service_id: string | null;
};