From 2af78e0a532674742a1cc83447cea3b1081f94b4 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Tue, 5 Aug 2025 11:42:56 -0400 Subject: [PATCH] feat: implement Wanderer integration with trail management and UI components; enhance settings for reauthentication --- .../integrations/views/integration_view.py | 12 +- .../integrations/views/wanderer_view.py | 11 +- frontend/src/lib/components/TrailCard.svelte | 108 +++++++ .../src/lib/components/WandererCard.svelte | 165 ++++++++++ .../components/locations/LocationMedia.svelte | 300 +++++++++++++----- frontend/src/lib/types.ts | 32 ++ frontend/src/routes/settings/+page.server.ts | 6 +- frontend/src/routes/settings/+page.svelte | 44 +++ 8 files changed, 597 insertions(+), 81 deletions(-) create mode 100644 frontend/src/lib/components/TrailCard.svelte create mode 100644 frontend/src/lib/components/WandererCard.svelte diff --git a/backend/server/integrations/views/integration_view.py b/backend/server/integrations/views/integration_view.py index 2e35934..7a391bd 100644 --- a/backend/server/integrations/views/integration_view.py +++ b/backend/server/integrations/views/integration_view.py @@ -2,6 +2,7 @@ import os from rest_framework.response import Response from rest_framework import viewsets, status from rest_framework.permissions import IsAuthenticated +from django.utils import timezone from integrations.models import ImmichIntegration, StravaToken, WandererIntegration from django.conf import settings @@ -17,6 +18,12 @@ class IntegrationView(viewsets.ViewSet): strava_integration_global = settings.STRAVA_CLIENT_ID != '' and settings.STRAVA_CLIENT_SECRET != '' strava_integration_user = StravaToken.objects.filter(user=request.user).exists() wanderer_integration = WandererIntegration.objects.filter(user=request.user).exists() + is_wanderer_expired = False + + if wanderer_integration: + token_expiry = WandererIntegration.objects.filter(user=request.user).first().token_expiry + if token_expiry and token_expiry < timezone.now(): + is_wanderer_expired = True return Response( { @@ -26,7 +33,10 @@ class IntegrationView(viewsets.ViewSet): 'global': strava_integration_global, 'user': strava_integration_user }, - 'wanderer': wanderer_integration + 'wanderer': { + 'exists': wanderer_integration, + 'expired': is_wanderer_expired + } }, status=status.HTTP_200_OK ) diff --git a/backend/server/integrations/views/wanderer_view.py b/backend/server/integrations/views/wanderer_view.py index aec5794..ffd8802 100644 --- a/backend/server/integrations/views/wanderer_view.py +++ b/backend/server/integrations/views/wanderer_view.py @@ -113,8 +113,8 @@ class WandererIntegrationViewSet(viewsets.ViewSet): "is_connected": True, }) - @action(detail=False, methods=["get"], url_path='list') - def list_trails(self, request): + @action(detail=False, methods=["get"], url_path='trails') + def trails(self, request): inst = self._get_obj() # Check if we need to prompt for password @@ -131,9 +131,12 @@ class WandererIntegrationViewSet(viewsets.ViewSet): }) raise ValidationError({"detail": str(e)}) - url = f"{inst.server_url.rstrip('/')}/api/v1/list" + # Pass along all query parameters except password + params = {k: v for k, v in request.query_params.items() if k != "password"} + + url = f"{inst.server_url.rstrip('/')}/api/v1/trail" try: - response = session.get(url, timeout=10) + response = session.get(url, params=params, timeout=10) response.raise_for_status() except requests.RequestException as exc: raise ValidationError({"detail": f"Error fetching trails: {exc}"}) diff --git a/frontend/src/lib/components/TrailCard.svelte b/frontend/src/lib/components/TrailCard.svelte new file mode 100644 index 0000000..f4062c0 --- /dev/null +++ b/frontend/src/lib/components/TrailCard.svelte @@ -0,0 +1,108 @@ + + +
+ {#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/WandererCard.svelte b/frontend/src/lib/components/WandererCard.svelte new file mode 100644 index 0000000..62c9c11 --- /dev/null +++ b/frontend/src/lib/components/WandererCard.svelte @@ -0,0 +1,165 @@ + + +
+
+
+ +
+ +
{trail.name}
+ + {trail.difficulty} + +
+ + + {#if trail.location} +
+ + {trail.location} +
+ {/if} + + +
+ {#if trail.distance} +
+ Distance: {formatDistance(trail.distance)} + {#if trail.duration > 0} + Duration: {Math.round(trail.duration / 60)} min + {/if} +
+ {/if} + + {#if trail.elevation_gain > 0 || trail.elevation_loss > 0} +
+ {#if trail.elevation_gain > 0} + ↗ {formatElevation(trail.elevation_gain)} + {/if} + {#if trail.elevation_loss > 0} + ↘ {formatElevation(trail.elevation_loss)} + {/if} +
+ {/if} + + {#if trail.waypoints && trail.waypoints.length > 0} +
+ Waypoints: {trail.waypoints.length} +
+ {/if} + + {#if trail.created} +
+ + Created: {formatDate(trail.created)} +
+ {/if} + + {#if trail.photos && trail.photos.length > 0} +
+ + {trail.photos.length} photo{trail.photos.length !== 1 ? 's' : ''} +
+ {/if} + + {#if trail.gpx} + + {/if} +
+ + + {#if trail.description} +
+

+ {stripHtml(trail.description).substring(0, 150)}... +

+
+ {/if} +
+ + +
+ +
+
+
+ + diff --git a/frontend/src/lib/components/locations/LocationMedia.svelte b/frontend/src/lib/components/locations/LocationMedia.svelte index 164fa32..b5d386c 100644 --- a/frontend/src/lib/components/locations/LocationMedia.svelte +++ b/frontend/src/lib/components/locations/LocationMedia.svelte @@ -1,5 +1,5 @@ @@ -899,18 +965,32 @@

Trails Management

- +
+ + +
Manage trails associated with this location. Trails can be linked to external services - like AllTrails or referenced by Wanderer ID. + like AllTrails or link to Wanderer trails.
@@ -930,15 +1010,7 @@ bind:value={trailLink} class="input input-bordered" placeholder="External link (e.g., AllTrails, Trailforks)" - disabled={isTrailLoading || trailWandererId.trim() !== ''} - /> -
OR
- {#if trailError}
@@ -956,9 +1028,7 @@
{/if} + + {#if showWandererForm} +
+

Add Wanderer Trail

+
+ {#if isWandererEnabled} +

+ Select a trail from your Wanderer account: +

+ + +
+ e.key === 'Enter' && handleSearch()} + /> +
+ {#if searchQuery} + + {/if} + +
+
+ + + {#if searchQuery && !isSearching} +

+ {wandererFetchedTrails.length} trail(s) found for "{searchQuery}" +

+ {/if} + + +
+ {#if isSearching} +
+ +
+ {:else if wandererFetchedTrails.length === 0} +
+ {#if searchQuery} + No trails found matching "{searchQuery}" + {:else} + No trails available + {/if} +
+ {:else} + {#each wandererFetchedTrails as trail (trail.id)} + + {/each} + {/if} +
+ {:else} +

+ Wanderer integration is not enabled or has expired. +

+ {/if} + +
+ +
+
+
+ {/if} + {#if trails.length > 0}
Current Trails
@@ -995,14 +1157,6 @@ placeholder="External link" disabled={editingTrailWandererId.trim() !== ''} /> -
OR
-
+ {#if !trail.wanderer_id} + + {/if}
+ {#if wandererEnabled && wandererExpired} +
+
+ + + +
+ + +
+ {/if} + {#if !wandererEnabled}