1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-08-07 14:15:18 +02:00

feat: enhance Wanderer integration with trail data fetching and UI updates; add measurement system support

This commit is contained in:
Sean Morley 2025-08-05 13:00:20 -04:00
parent 65f99c5a50
commit 546ae7aa2e
6 changed files with 170 additions and 111 deletions

View file

@ -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()

View file

@ -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}")
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()

View file

@ -281,6 +281,7 @@
steps[2].selected = false;
steps[3].selected = true;
}}
measurementSystem={user?.measurement_system || 'metric'}
/>
{/if}
{#if steps[3].selected}

View file

@ -1,108 +0,0 @@
<script lang="ts"></script>
<div class="relative group">
{#if trailToEdit?.id === trail.id}
<!-- Edit Mode -->
<div class="bg-warning/10 p-4 rounded-lg border border-warning/30">
<div class="flex items-center gap-2 mb-3">
<EditIcon class="w-4 h-4 text-warning" />
<span class="text-sm font-medium text-warning">Editing Trail</span>
</div>
<div class="grid gap-3">
<input
type="text"
bind:value={editingTrailName}
class="input input-bordered input-sm"
placeholder="Trail name"
/>
<input
type="url"
bind:value={editingTrailLink}
class="input input-bordered input-sm"
placeholder="External link"
disabled={editingTrailWandererId.trim() !== ''}
/>
<div class="text-center text-xs text-base-content/60">OR</div>
<input
type="text"
bind:value={editingTrailWandererId}
class="input input-bordered input-sm"
placeholder="Wanderer Trail ID"
disabled={editingTrailLink.trim() !== ''}
/>
</div>
<div class="flex gap-2 mt-3">
<button
class="btn btn-success btn-xs flex-1"
disabled={!validateEditTrailForm()}
on:click={saveTrailEdit}
>
<CheckIcon class="w-3 h-3" />
Save
</button>
<button class="btn btn-ghost btn-xs flex-1" on:click={cancelEditingTrail}>
<CloseIcon class="w-3 h-3" />
Cancel
</button>
</div>
</div>
{:else}
<!-- Normal Display -->
<div
class="bg-base-50 p-4 rounded-lg border border-base-200 hover:border-base-300 transition-colors"
>
<div class="flex items-center gap-3 mb-3">
<div class="p-2 bg-accent/10 rounded">
{#if trail.wanderer_id}
<Star class="w-4 h-4 text-accent" />
{:else}
<LinkIcon class="w-4 h-4 text-accent" />
{/if}
</div>
<div class="flex-1 min-w-0">
<div class="font-medium truncate">{trail.name}</div>
<div class="text-xs text-accent/70 mt-1">
{trail.provider || 'External'}
</div>
</div>
</div>
{#if trail.link}
<a
href={trail.link}
target="_blank"
rel="noopener noreferrer"
class="text-xs text-accent hover:text-accent-focus mb-3 break-all block underline"
>
{trail.link}
</a>
{:else if trail.wanderer_id}
<div class="text-xs text-base-content/60 mb-3 break-all">
Wanderer ID: {trail.wanderer_id}
</div>
{:else}
<div class="text-xs text-base-content/40 mb-3 italic">No external link available</div>
{/if}
<!-- Trail Controls -->
<div class="flex gap-2 justify-end">
<button
type="button"
class="btn btn-warning btn-xs btn-square tooltip tooltip-top"
data-tip="Edit Trail"
on:click={() => startEditingTrail(trail)}
>
<EditIcon class="w-3 h-3" />
</button>
<button
type="button"
class="btn btn-error btn-xs btn-square tooltip tooltip-top"
data-tip="Remove Trail"
on:click={() => removeTrail(trail.id)}
>
<TrashIcon class="w-3 h-3" />
</button>
</div>
</div>
{/if}
</div>

View file

@ -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 @@
<div
class="bg-base-50 p-4 rounded-lg border border-base-200 hover:border-base-300 transition-colors"
>
<!-- Header -->
<div class="flex items-center gap-3 mb-3">
<div class="p-2 bg-accent/10 rounded">
{#if trail.wanderer_id}
@ -1194,6 +1233,90 @@
</div>
</div>
<!-- Wanderer Trail Enhanced Data -->
{#if trail.wanderer_data}
<div class="mb-4 space-y-3">
<!-- Trail Stats -->
<div class="grid grid-cols-2 gap-3">
<div class="flex items-center gap-2 text-sm">
<MapPin class="w-3 h-3 text-base-content/60" />
<span class="text-base-content/80">
{getDistance(trail.wanderer_data.distance)}
</span>
</div>
{#if trail.wanderer_data.duration > 0}
<div class="flex items-center gap-2 text-sm">
<Clock class="w-3 h-3 text-base-content/60" />
<span class="text-base-content/80">
{getDuration(trail.wanderer_data.duration)}
</span>
</div>
{/if}
{#if trail.wanderer_data.elevation_gain > 0}
<div class="flex items-center gap-2 text-sm">
<TrendingUp class="w-3 h-3 text-base-content/60" />
<span class="text-base-content/80">
{getElevation(trail.wanderer_data.elevation_gain)} gain
</span>
</div>
{/if}
<div class="flex items-center gap-2 text-sm">
<Calendar class="w-3 h-3 text-base-content/60" />
<span class="text-base-content/80">
{formatDate(trail.wanderer_data.date)}
</span>
</div>
</div>
<!-- Difficulty Badge -->
{#if trail.wanderer_data.difficulty}
<div class="flex items-center gap-2">
<span
class="badge badge-sm"
class:badge-success={trail.wanderer_data.difficulty === 'easy'}
class:badge-warning={trail.wanderer_data.difficulty === 'moderate'}
class:badge-error={trail.wanderer_data.difficulty === 'hard'}
>
{trail.wanderer_data.difficulty}
</span>
{#if trail.wanderer_data.like_count > 0}
<div class="flex items-center gap-1 text-xs text-base-content/60">
<Users class="w-3 h-3" />
{trail.wanderer_data.like_count} likes
</div>
{/if}
</div>
{/if}
<!-- Description -->
{#if trail.wanderer_data.description}
<div class="text-sm text-base-content/70 leading-relaxed">
{@html trail.wanderer_data.description}
</div>
{/if}
<!-- Location -->
{#if trail.wanderer_data.location}
<div class="text-xs text-base-content/60 flex items-center gap-1">
<MapPin class="w-3 h-3" />
{trail.wanderer_data.location}
</div>
{/if}
<!-- Photos indicator -->
{#if trail.wanderer_data.photos && trail.wanderer_data.photos.length > 0}
<div class="flex items-center gap-1 text-xs text-base-content/60">
<Camera class="w-3 h-3" />
{trail.wanderer_data.photos.length} photo{trail.wanderer_data.photos
.length > 1
? 's'
: ''}
</div>
{/if}
</div>
{/if}
<!-- External Link -->
{#if trail.link}
<a
href={trail.link}

View file

@ -329,6 +329,7 @@ export type Trail = {
link?: string | null; // Optional link to the trail
wanderer_id?: string | null; // Optional ID for integration with Wanderer
provider: string; // Provider of the trail data, e.g., 'wanderer', 'external'
wanderer_data: WandererTrail | null; // Optional data from Wanderer integration
};
export type StravaActivity = {
@ -464,5 +465,4 @@ export type WandererTrail = {
thumbnail: number;
updated: string; // ISO 8601 date string
waypoints: string[];
expand: any;
};