mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-08-10 07:35:17 +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
|
from integrations.models import ImmichIntegration
|
||||||
import gpxpy
|
import gpxpy
|
||||||
import geojson
|
import geojson
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ContentImageSerializer(CustomModelSerializer):
|
class ContentImageSerializer(CustomModelSerializer):
|
||||||
|
@ -88,9 +91,10 @@ class CategorySerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class TrailSerializer(CustomModelSerializer):
|
class TrailSerializer(CustomModelSerializer):
|
||||||
provider = serializers.SerializerMethodField()
|
provider = serializers.SerializerMethodField()
|
||||||
|
wanderer_data = serializers.SerializerMethodField()
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Trail
|
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']
|
read_only_fields = ['id', 'created_at', 'user', 'provider']
|
||||||
|
|
||||||
def get_provider(self, obj):
|
def get_provider(self, obj):
|
||||||
|
@ -108,6 +112,29 @@ class TrailSerializer(CustomModelSerializer):
|
||||||
return 'Outdooractive'
|
return 'Outdooractive'
|
||||||
return 'External Link'
|
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):
|
class ActivitySerializer(CustomModelSerializer):
|
||||||
geojson = serializers.SerializerMethodField()
|
geojson = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
|
|
@ -108,4 +108,20 @@ def make_wanderer_request(integration: WandererIntegration, endpoint: str, metho
|
||||||
return response
|
return response
|
||||||
except requests.RequestException as exc:
|
except requests.RequestException as exc:
|
||||||
logger.error(f"Error making {method} request to {url}: {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[2].selected = false;
|
||||||
steps[3].selected = true;
|
steps[3].selected = true;
|
||||||
}}
|
}}
|
||||||
|
measurementSystem={user?.measurement_system || 'metric'}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if steps[3].selected}
|
{#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 SwapHorizontalVariantIcon from '~icons/mdi/swap-horizontal-variant';
|
||||||
import LinkIcon from '~icons/mdi/link';
|
import LinkIcon from '~icons/mdi/link';
|
||||||
import PlusIcon from '~icons/mdi/plus';
|
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 { addToast } from '$lib/toasts';
|
||||||
import ImmichSelect from '../ImmichSelect.svelte';
|
import ImmichSelect from '../ImmichSelect.svelte';
|
||||||
|
@ -30,6 +36,7 @@
|
||||||
export let attachments: Attachment[] = [];
|
export let attachments: Attachment[] = [];
|
||||||
export let trails: Trail[] = [];
|
export let trails: Trail[] = [];
|
||||||
export let itemId: string = '';
|
export let itemId: string = '';
|
||||||
|
export let measurementSystem: 'metric' | 'imperial' = 'metric';
|
||||||
|
|
||||||
// Component state
|
// Component state
|
||||||
let fileInput: HTMLInputElement;
|
let fileInput: HTMLInputElement;
|
||||||
|
@ -462,6 +469,37 @@
|
||||||
editingTrailWandererId = trail.wanderer_id || '';
|
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 = '') {
|
async function fetchWandererTrails(filter = '') {
|
||||||
isSearching = true;
|
isSearching = true;
|
||||||
try {
|
try {
|
||||||
|
@ -1178,6 +1216,7 @@
|
||||||
<div
|
<div
|
||||||
class="bg-base-50 p-4 rounded-lg border border-base-200 hover:border-base-300 transition-colors"
|
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="flex items-center gap-3 mb-3">
|
||||||
<div class="p-2 bg-accent/10 rounded">
|
<div class="p-2 bg-accent/10 rounded">
|
||||||
{#if trail.wanderer_id}
|
{#if trail.wanderer_id}
|
||||||
|
@ -1194,6 +1233,90 @@
|
||||||
</div>
|
</div>
|
||||||
</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}
|
{#if trail.link}
|
||||||
<a
|
<a
|
||||||
href={trail.link}
|
href={trail.link}
|
||||||
|
|
|
@ -329,6 +329,7 @@ export type Trail = {
|
||||||
link?: string | null; // Optional link to the trail
|
link?: string | null; // Optional link to the trail
|
||||||
wanderer_id?: string | null; // Optional ID for integration with Wanderer
|
wanderer_id?: string | null; // Optional ID for integration with Wanderer
|
||||||
provider: string; // Provider of the trail data, e.g., 'wanderer', 'external'
|
provider: string; // Provider of the trail data, e.g., 'wanderer', 'external'
|
||||||
|
wanderer_data: WandererTrail | null; // Optional data from Wanderer integration
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StravaActivity = {
|
export type StravaActivity = {
|
||||||
|
@ -464,5 +465,4 @@ export type WandererTrail = {
|
||||||
thumbnail: number;
|
thumbnail: number;
|
||||||
updated: string; // ISO 8601 date string
|
updated: string; // ISO 8601 date string
|
||||||
waypoints: string[];
|
waypoints: string[];
|
||||||
expand: any;
|
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue