From 546ae7aa2ed7d1c32f7dabc40a15e5814588b434 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Tue, 5 Aug 2025 13:00:20 -0400 Subject: [PATCH] feat: enhance Wanderer integration with trail data fetching and UI updates; add measurement system support --- backend/server/adventures/serializers.py | 29 ++++- .../server/integrations/wanderer_services.py | 18 ++- .../lib/components/NewLocationModal.svelte | 1 + frontend/src/lib/components/TrailCard.svelte | 108 --------------- .../components/locations/LocationMedia.svelte | 123 ++++++++++++++++++ frontend/src/lib/types.ts | 2 +- 6 files changed, 170 insertions(+), 111 deletions(-) delete mode 100644 frontend/src/lib/components/TrailCard.svelte diff --git a/backend/server/adventures/serializers.py b/backend/server/adventures/serializers.py index db01245..cd065b2 100644 --- a/backend/server/adventures/serializers.py +++ b/backend/server/adventures/serializers.py @@ -9,6 +9,9 @@ from geopy.distance import geodesic from integrations.models import ImmichIntegration import gpxpy import geojson +import logging + +logger = logging.getLogger(__name__) class ContentImageSerializer(CustomModelSerializer): @@ -88,9 +91,10 @@ class CategorySerializer(serializers.ModelSerializer): class TrailSerializer(CustomModelSerializer): provider = serializers.SerializerMethodField() + wanderer_data = serializers.SerializerMethodField() class Meta: model = Trail - fields = ['id', 'user', 'name', 'location', 'created_at','link','wanderer_id', 'provider'] + fields = ['id', 'user', 'name', 'location', 'created_at','link','wanderer_id', 'provider', 'wanderer_data'] read_only_fields = ['id', 'created_at', 'user', 'provider'] def get_provider(self, obj): @@ -108,6 +112,29 @@ class TrailSerializer(CustomModelSerializer): return 'Outdooractive' return 'External Link' + def get_wanderer_data(self, obj): + if not obj.wanderer_id: + return None + + # Fetch the Wanderer trail data + from integrations.models import WandererIntegration + from integrations.wanderer_services import fetch_trail_by_id + try: + integration = WandererIntegration.objects.filter(user=obj.user).first() + if not integration: + return None + + # Assuming there's a method to fetch trail data by ID + trail_data = fetch_trail_by_id(integration, obj.wanderer_id) + if not trail_data: + return None + obj.wanderer_data = trail_data + return trail_data + except Exception as e: + logger.error(f"Error fetching Wanderer trail data for {obj.wanderer_id}") + return None + + class ActivitySerializer(CustomModelSerializer): geojson = serializers.SerializerMethodField() diff --git a/backend/server/integrations/wanderer_services.py b/backend/server/integrations/wanderer_services.py index 5c2e68d..30b9fb1 100644 --- a/backend/server/integrations/wanderer_services.py +++ b/backend/server/integrations/wanderer_services.py @@ -108,4 +108,20 @@ def make_wanderer_request(integration: WandererIntegration, endpoint: str, metho return response except requests.RequestException as exc: logger.error(f"Error making {method} request to {url}: {exc}") - raise IntegrationError(f"Error communicating with Wanderer: {exc}") \ No newline at end of file + raise IntegrationError(f"Error communicating with Wanderer: {exc}") + +# function to fetch a specific trail by ID +def fetch_trail_by_id(integration: WandererIntegration, trail_id: str, password_for_reauth: str = None): + """ + Fetch a specific trail by its ID from the Wanderer API. + + Args: + integration: WandererIntegration instance + trail_id: ID of the trail to fetch + password_for_reauth: Password to use if re-authentication is needed + + Returns: + dict: Trail data from the API + """ + response = make_wanderer_request(integration, f"/api/v1/trail/{trail_id}", password_for_reauth=password_for_reauth) + return response.json() \ No newline at end of file diff --git a/frontend/src/lib/components/NewLocationModal.svelte b/frontend/src/lib/components/NewLocationModal.svelte index 91ba2d0..e09f853 100644 --- a/frontend/src/lib/components/NewLocationModal.svelte +++ b/frontend/src/lib/components/NewLocationModal.svelte @@ -281,6 +281,7 @@ steps[2].selected = false; steps[3].selected = true; }} + measurementSystem={user?.measurement_system || 'metric'} /> {/if} {#if steps[3].selected} diff --git a/frontend/src/lib/components/TrailCard.svelte b/frontend/src/lib/components/TrailCard.svelte deleted file mode 100644 index f4062c0..0000000 --- a/frontend/src/lib/components/TrailCard.svelte +++ /dev/null @@ -1,108 +0,0 @@ - - -
- {#if trailToEdit?.id === trail.id} - -
-
- - Editing Trail -
-
- - -
OR
- -
-
- - -
-
- {:else} - -
-
-
- {#if trail.wanderer_id} - - {:else} - - {/if} -
-
-
{trail.name}
-
- {trail.provider || 'External'} -
-
-
- - {#if trail.link} - - {trail.link} - - {:else if trail.wanderer_id} -
- Wanderer ID: {trail.wanderer_id} -
- {:else} -
No external link available
- {/if} - - -
- - -
-
- {/if} -
diff --git a/frontend/src/lib/components/locations/LocationMedia.svelte b/frontend/src/lib/components/locations/LocationMedia.svelte index b5d386c..7ff28cb 100644 --- a/frontend/src/lib/components/locations/LocationMedia.svelte +++ b/frontend/src/lib/components/locations/LocationMedia.svelte @@ -20,6 +20,12 @@ import SwapHorizontalVariantIcon from '~icons/mdi/swap-horizontal-variant'; import LinkIcon from '~icons/mdi/link'; import PlusIcon from '~icons/mdi/plus'; + import MapPin from '~icons/mdi/map-marker'; + import Clock from '~icons/mdi/clock'; + import TrendingUp from '~icons/mdi/trending-up'; + import Calendar from '~icons/mdi/calendar'; + import Users from '~icons/mdi/account-supervisor'; + import Camera from '~icons/mdi/camera'; import { addToast } from '$lib/toasts'; import ImmichSelect from '../ImmichSelect.svelte'; @@ -30,6 +36,7 @@ export let attachments: Attachment[] = []; export let trails: Trail[] = []; export let itemId: string = ''; + export let measurementSystem: 'metric' | 'imperial' = 'metric'; // Component state let fileInput: HTMLInputElement; @@ -462,6 +469,37 @@ editingTrailWandererId = trail.wanderer_id || ''; } + function getDistance(meters: number) { + // Convert meters to miles based measurement system + if (measurementSystem === 'imperial') { + return `${(meters * 0.000621371).toFixed(2)} mi`; + } else { + return `${(meters / 1000).toFixed(2)} km`; + } + } + + function getElevation(meters: number) { + // Convert meters to feet based measurement system + if (measurementSystem === 'imperial') { + return `${(meters * 3.28084).toFixed(1)} ft`; + } else { + return `${meters.toFixed(1)} m`; + } + } + + function getDuration(minutes: number) { + const hours = Math.floor(minutes / 60); + const mins = minutes % 60; + if (hours > 0) { + return `${hours}h ${mins}m`; + } + return `${mins}m`; + } + + function formatDate(dateString: string | number | Date) { + return new Date(dateString).toLocaleDateString(); + } + async function fetchWandererTrails(filter = '') { isSearching = true; try { @@ -1178,6 +1216,7 @@
+
{#if trail.wanderer_id} @@ -1194,6 +1233,90 @@
+ + {#if trail.wanderer_data} +
+ +
+
+ + + {getDistance(trail.wanderer_data.distance)} + +
+ {#if trail.wanderer_data.duration > 0} +
+ + + {getDuration(trail.wanderer_data.duration)} + +
+ {/if} + {#if trail.wanderer_data.elevation_gain > 0} +
+ + + {getElevation(trail.wanderer_data.elevation_gain)} gain + +
+ {/if} +
+ + + {formatDate(trail.wanderer_data.date)} + +
+
+ + + {#if trail.wanderer_data.difficulty} +
+ + {trail.wanderer_data.difficulty} + + {#if trail.wanderer_data.like_count > 0} +
+ + {trail.wanderer_data.like_count} likes +
+ {/if} +
+ {/if} + + + {#if trail.wanderer_data.description} +
+ {@html trail.wanderer_data.description} +
+ {/if} + + + {#if trail.wanderer_data.location} +
+ + {trail.wanderer_data.location} +
+ {/if} + + + {#if trail.wanderer_data.photos && trail.wanderer_data.photos.length > 0} +
+ + {trail.wanderer_data.photos.length} photo{trail.wanderer_data.photos + .length > 1 + ? 's' + : ''} +
+ {/if} +
+ {/if} + + {#if trail.link}