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:
parent
c7ff8f4bc7
commit
5046bd49f7
13 changed files with 1626 additions and 255 deletions
|
@ -285,6 +285,7 @@
|
|||
{#if steps[3].selected}
|
||||
<LocationVisits
|
||||
bind:visits={location.visits}
|
||||
bind:trails={location.trails}
|
||||
objectId={location.id}
|
||||
type="location"
|
||||
on:back={() => {
|
||||
|
|
205
frontend/src/lib/components/StravaActivityCard.svelte
Normal file
205
frontend/src/lib/components/StravaActivityCard.svelte
Normal 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> {activity.average_cadence.toFixed(1)}
|
||||
</div>
|
||||
{/if}
|
||||
{#if activity.calories}
|
||||
<div class="badge badge-ghost">
|
||||
<span class="font-medium">Calories:</span> {activity.calories}
|
||||
</div>
|
||||
{/if}
|
||||
{#if activity.kudos_count > 0}
|
||||
<div class="badge badge-ghost">
|
||||
<span class="font-medium">Kudos:</span> {activity.kudos_count}
|
||||
</div>
|
||||
{/if}
|
||||
{#if activity.achievement_count > 0}
|
||||
<div class="badge badge-success badge-outline">
|
||||
<span class="font-medium">Achievements:</span> {activity.achievement_count}
|
||||
</div>
|
||||
{/if}
|
||||
{#if activity.pr_count > 0}
|
||||
<div class="badge badge-warning badge-outline">
|
||||
<span class="font-medium">PRs:</span> {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
|
@ -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;
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue