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:
parent
65f99c5a50
commit
546ae7aa2e
6 changed files with 170 additions and 111 deletions
|
@ -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()
|
||||
|
||||
|
|
|
@ -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()
|
|
@ -281,6 +281,7 @@
|
|||
steps[2].selected = false;
|
||||
steps[3].selected = true;
|
||||
}}
|
||||
measurementSystem={user?.measurement_system || 'metric'}
|
||||
/>
|
||||
{/if}
|
||||
{#if steps[3].selected}
|
||||
|
|
|
@ -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>
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue