1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-08-08 06:35:19 +02:00

Refactor Location and Visit types: Replace visits structure in Location with Visit type and add location, created_at, and updated_at fields to Visit

This commit is contained in:
Sean Morley 2025-08-07 11:52:31 -04:00
parent b7b7f9d26d
commit 1395efa389
12 changed files with 881 additions and 2078 deletions

View file

@ -109,6 +109,9 @@ class IsOwnerOrSharedWithFullAccess(permissions.BasePermission):
is_safe_method = request.method in permissions.SAFE_METHODS
# If the object has a location field, get that location and continue checking with that object, basically from the location's perspective. I am very proud of this line of code and that's why I am writing this comment.
print("Checking permissions for object", obj, "of type", type(obj).__name__)
if type(obj).__name__ == 'Trail':
obj = obj.location
@ -117,6 +120,13 @@ class IsOwnerOrSharedWithFullAccess(permissions.BasePermission):
if hasattr(obj, 'visit') and hasattr(obj.visit, 'location'):
obj = obj.visit.location
if type(obj).__name__ == 'Visit':
print("Checking permissions for Visit object", obj)
# If the object is a Visit, get its location
if hasattr(obj, 'location'):
obj = obj.location
# Anonymous users only get read access to public objects
if not user or not user.is_authenticated:
return is_safe_method and getattr(obj, 'is_public', False)

View file

@ -215,8 +215,13 @@ class VisitSerializer(serializers.ModelSerializer):
class Meta:
model = Visit
fields = ['id', 'start_date', 'end_date', 'timezone', 'notes', 'activities']
read_only_fields = ['id']
fields = ['id', 'start_date', 'end_date', 'timezone', 'notes', 'activities','location', 'created_at', 'updated_at']
read_only_fields = ['id', 'created_at', 'updated_at']
def create(self, validated_data):
if not validated_data.get('end_date') and validated_data.get('start_date'):
validated_data['end_date'] = validated_data['start_date']
return super().create(validated_data)
class LocationSerializer(CustomModelSerializer):
images = serializers.SerializerMethodField()
@ -358,17 +363,12 @@ class LocationSerializer(CustomModelSerializer):
return obj.is_visited_status()
def create(self, validated_data):
visits_data = validated_data.pop('visits', [])
category_data = validated_data.pop('category', None)
collections_data = validated_data.pop('collections', [])
print(category_data)
location = Location.objects.create(**validated_data)
# Handle visits
for visit_data in visits_data:
Visit.objects.create(location=location, **visit_data)
# Handle category
if category_data:
category = self.get_or_create_category(category_data)
@ -384,7 +384,6 @@ class LocationSerializer(CustomModelSerializer):
def update(self, instance, validated_data):
has_visits = 'visits' in validated_data
visits_data = validated_data.pop('visits', [])
category_data = validated_data.pop('category', None)
collections_data = validated_data.pop('collections', None)
@ -405,27 +404,6 @@ class LocationSerializer(CustomModelSerializer):
if collections_data is not None:
instance.collections.set(collections_data)
# Handle visits
if has_visits:
current_visits = instance.visits.all()
current_visit_ids = set(current_visits.values_list('id', flat=True))
updated_visit_ids = set()
for visit_data in visits_data:
visit_id = visit_data.get('id')
if visit_id and visit_id in current_visit_ids:
visit = current_visits.get(id=visit_id)
for attr, value in visit_data.items():
setattr(visit, attr, value)
visit.save()
updated_visit_ids.add(visit_id)
else:
new_visit = Visit.objects.create(location=instance, **visit_data)
updated_visit_ids.add(new_visit.id)
visits_to_delete = current_visit_ids - updated_visit_ids
instance.visits.filter(id__in=visits_to_delete).delete()
# call save on the location to update the updated_at field and trigger any geocoding
instance.save()

View file

@ -22,6 +22,7 @@ router.register(r'recommendations', RecommendationsViewSet, basename='recommenda
router.register(r'backup', BackupViewSet, basename='backup')
router.register(r'trails', TrailViewSet, basename='trails')
router.register(r'activities', ActivityViewSet, basename='activities')
router.register(r'visits', VisitViewSet, basename='visits')
urlpatterns = [
# Include the router under the 'api/' prefix

View file

@ -17,3 +17,4 @@ from .recommendations_view import *
from .import_export_view import *
from .trail_view import *
from .activity_view import *
from .visit_view import *

View file

@ -3,6 +3,7 @@ from django.db.models import Q
from adventures.models import Location, Activity
from adventures.serializers import ActivitySerializer
from adventures.permissions import IsOwnerOrSharedWithFullAccess
from rest_framework.exceptions import PermissionDenied
class ActivityViewSet(viewsets.ModelViewSet):
serializer_class = ActivitySerializer
@ -37,4 +38,32 @@ class ActivityViewSet(viewsets.ModelViewSet):
"""
Set the user when creating an activity.
"""
visit = serializer.validated_data.get('visit')
location = visit.location if visit else None
if location and not IsOwnerOrSharedWithFullAccess().has_object_permission(self.request, self, location):
raise PermissionDenied("You do not have permission to add an activity to this location.")
serializer.save(user=self.request.user)
def perform_update(self, serializer):
instance = serializer.instance
new_visit = serializer.validated_data.get('visit')
# Prevent changing visit/location after creation
if new_visit and new_visit != instance.visit:
raise PermissionDenied("Cannot change activity visit after creation. Create a new activity instead.")
# Check permission for updates to the existing location
location = instance.visit.location if instance.visit else None
if location and not IsOwnerOrSharedWithFullAccess().has_object_permission(self.request, self, location):
raise PermissionDenied("You do not have permission to update this activity.")
serializer.save()
def perform_destroy(self, instance):
location = instance.visit.location if instance.visit else None
if location and not IsOwnerOrSharedWithFullAccess().has_object_permission(self.request, self, location):
raise PermissionDenied("You do not have permission to delete this activity.")
instance.delete()

View file

@ -3,6 +3,7 @@ from django.db.models import Q
from adventures.models import Location, Trail
from adventures.serializers import TrailSerializer
from adventures.permissions import IsOwnerOrSharedWithFullAccess
from rest_framework.exceptions import PermissionDenied
class TrailViewSet(viewsets.ModelViewSet):
serializer_class = TrailSerializer
@ -34,7 +35,32 @@ class TrailViewSet(viewsets.ModelViewSet):
return Trail.objects.filter(location_filter).distinct()
def perform_create(self, serializer):
"""
Set the user when creating a trail.
"""
location = serializer.validated_data.get('location')
# Optional: import this if not in the same file
# from adventures.permissions import IsOwnerOrSharedWithFullAccess
if not IsOwnerOrSharedWithFullAccess().has_object_permission(self.request, self, location):
raise PermissionDenied("You do not have permission to add a trail to this location.")
serializer.save(user=self.request.user)
def perform_update(self, serializer):
instance = serializer.instance
new_location = serializer.validated_data.get('location')
# Prevent changing location after creation
if new_location and new_location != instance.location:
raise PermissionDenied("Cannot change trail location after creation. Create a new trail instead.")
# Check permission for updates to the existing location
if not IsOwnerOrSharedWithFullAccess().has_object_permission(self.request, self, instance.location):
raise PermissionDenied("You do not have permission to update this trail.")
serializer.save()
def perform_destroy(self, instance):
if not IsOwnerOrSharedWithFullAccess().has_object_permission(self.request, self, instance.location):
raise PermissionDenied("You do not have permission to delete this trail.")
instance.delete()

View file

@ -0,0 +1,66 @@
from rest_framework import viewsets
from django.db.models import Q
from adventures.models import Location, Visit
from adventures.serializers import VisitSerializer
from adventures.permissions import IsOwnerOrSharedWithFullAccess
from rest_framework.exceptions import PermissionDenied
class VisitViewSet(viewsets.ModelViewSet):
serializer_class = VisitSerializer
permission_classes = [IsOwnerOrSharedWithFullAccess]
def get_queryset(self):
"""
Returns visits based on location permissions.
Users can only see visits in locations they have access to for editing/updating/deleting.
This means they are either:
- The owner of the location
- The location is in a collection that is shared with the user
- The location is in a collection that the user owns
"""
user = self.request.user
if not user or not user.is_authenticated:
return Visit.objects.none()
# Build the filter for accessible locations
location_filter = Q(location__user=user) # User owns the location
# Location is in collections (many-to-many) that are shared with user
location_filter |= Q(location__collections__shared_with=user)
# Location is in collections (many-to-many) that user owns
location_filter |= Q(location__collections__user=user)
return Visit.objects.filter(location_filter).distinct()
def perform_create(self, serializer):
"""
Set the user when creating a visit and check permissions.
"""
location = serializer.validated_data.get('location')
if not IsOwnerOrSharedWithFullAccess().has_object_permission(self.request, self, location):
raise PermissionDenied("You do not have permission to add a visit to this location.")
serializer.save()
def perform_update(self, serializer):
instance = serializer.instance
new_location = serializer.validated_data.get('location')
# Prevent changing location after creation
if new_location and new_location != instance.location:
raise PermissionDenied("Cannot change visit location after creation. Create a new visit instead.")
# Check permission for updates to the existing location
if not IsOwnerOrSharedWithFullAccess().has_object_permission(self.request, self, instance.location):
raise PermissionDenied("You do not have permission to update this visit.")
serializer.save()
def perform_destroy(self, instance):
if not IsOwnerOrSharedWithFullAccess().has_object_permission(self.request, self, instance.location):
raise PermissionDenied("You do not have permission to delete this visit.")
instance.delete()

View file

@ -39,6 +39,9 @@
function custom_category() {
new_category.name = new_category.display_name.toLowerCase().replace(/ /g, '_');
if (!new_category.icon) {
new_category.icon = '🌎'; // Default icon if none selected
}
selectCategory(new_category);
}

File diff suppressed because it is too large Load diff

View file

@ -298,7 +298,6 @@
bind:visits={location.visits}
bind:trails={location.trails}
objectId={location.id}
type="location"
on:back={() => {
steps[3].selected = false;
steps[2].selected = true;

View file

@ -36,13 +36,12 @@
// Props
export let collection: Collection | null = null;
export let type: 'location' | 'transportation' | 'lodging' = 'location';
export let selectedStartTimezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone;
export let selectedEndTimezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone;
export let utcStartDate: string | null = null;
export let utcEndDate: string | null = null;
export let note: string | null = null;
export let visits: (Visit | TransportationVisit)[] | null = null;
export let visits: Visit[] | null = null;
export let objectId: string;
export let trails: Trail[] = [];
export let measurementSystem: 'metric' | 'imperial' = 'metric';
@ -59,6 +58,7 @@
let fullEndDate: string = '';
let constrainDates: boolean = false;
let isEditing = false;
let visitIdEditing: string | null = null;
// Activity management state
let stravaEnabled: boolean = false;
@ -116,6 +116,15 @@
'Other'
];
function getTypeConfig() {
return {
startLabel: 'Start Date',
endLabel: 'End Date',
icon: CalendarIcon,
color: 'primary'
};
}
// Reactive constraints
$: constraintStartDate = allDay
? fullStartDate && fullStartDate.includes('T')
@ -147,7 +156,7 @@
const end = updateLocalDate({
utcDate: utcEndDate,
timezone: type === 'transportation' ? selectedEndTimezone : selectedStartTimezone
timezone: selectedEndTimezone
}).localDate;
localStartDate = start;
@ -193,32 +202,6 @@
return 0;
}
function getTypeConfig() {
switch (type) {
case 'transportation':
return {
startLabel: 'Departure Date',
endLabel: 'Arrival Date',
icon: MapMarkerIcon,
color: 'accent'
};
case 'lodging':
return {
startLabel: 'Check In',
endLabel: 'Check Out',
icon: CalendarIcon,
color: 'secondary'
};
default:
return {
startLabel: 'Start Date',
endLabel: 'End Date',
icon: CalendarIcon,
color: 'primary'
};
}
}
// Event handlers
function handleLocalDateChange() {
utcStartDate = updateUTCDate({
@ -229,7 +212,7 @@
utcEndDate = updateUTCDate({
localDate: localEndDate,
timezone: type === 'transportation' ? selectedEndTimezone : selectedStartTimezone,
timezone: selectedEndTimezone,
allDay
}).utcDate;
}
@ -251,7 +234,7 @@
utcEndDate = updateUTCDate({
localDate: localEndDate,
timezone: type === 'transportation' ? selectedEndTimezone : selectedStartTimezone,
timezone: selectedEndTimezone,
allDay
}).utcDate;
@ -262,74 +245,59 @@
localEndDate = updateLocalDate({
utcDate: utcEndDate,
timezone: type === 'transportation' ? selectedEndTimezone : selectedStartTimezone
timezone: selectedEndTimezone
}).localDate;
}
function createVisitObject(): Visit | TransportationVisit {
const uniqueId = Date.now().toString(36) + Math.random().toString(36).substring(2);
if (type === 'transportation') {
const transportVisit: TransportationVisit = {
id: uniqueId,
start_date: utcStartDate ?? '',
end_date: utcEndDate ?? utcStartDate ?? '',
notes: note ?? '',
start_timezone: selectedStartTimezone,
end_timezone: selectedEndTimezone,
activities: []
};
return transportVisit;
} else {
const regularVisit: Visit = {
id: uniqueId,
start_date: utcStartDate ?? '',
end_date: utcEndDate ?? utcStartDate ?? '',
notes: note ?? '',
timezone: selectedStartTimezone,
activities: []
};
return regularVisit;
}
}
async function addVisit() {
const newVisit = createVisitObject();
// Patch updated visits array to location and get the response with actual IDs
console.log('Adding new visit:', newVisit);
console.log(objectId);
if (type === 'location' && objectId) {
try {
const updatedVisits = visits ? [...visits, newVisit] : [newVisit];
console.log('Patching visits:', updatedVisits);
const response = await fetch(`/api/locations/${objectId}/`, {
// If editing an existing visit, patch instead of creating new
if (visitIdEditing) {
const response = await fetch(`/api/visits/${visitIdEditing}/`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ visits: updatedVisits })
body: JSON.stringify({
start_date: utcStartDate,
end_date: utcEndDate,
notes: note,
timezone: selectedStartTimezone
})
});
if (response.ok) {
const updatedLocation = await response.json();
// Update visits with the response data that contains actual IDs
visits = updatedLocation.visits;
const updatedVisit: Visit = await response.json();
visits = visits ? [...visits, updatedVisit] : [updatedVisit];
dispatch('visitAdded', updatedVisit);
visitIdEditing = null;
} else {
console.error('Failed to patch visits:', await response.text());
return; // Don't update local state if API call failed
}
} catch (error) {
console.error('Error patching visits:', error);
return; // Don't update local state if API call failed
const errorText = await response.text();
alert(`Failed to update visit: ${errorText}`);
}
} else {
// Fallback for non-location types - add new visit to the visits array
if (visits) {
visits = [...visits, newVisit];
// post to /api/visits for new visit
const response = await fetch('/api/visits/', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
object_id: objectId,
start_date: utcStartDate,
end_date: utcEndDate,
notes: note,
timezone: selectedStartTimezone,
location: objectId
})
});
if (response.ok) {
const newVisit: Visit = await response.json();
visits = visits ? [...visits, newVisit] : [newVisit];
dispatch('visitAdded', newVisit);
} else {
visits = [newVisit];
const errorText = await response.text();
alert(`Failed to add visit: ${errorText}`);
}
}
@ -581,7 +549,7 @@
if (response.ok) {
// Refetch the location data to get the updated visits with correct IDs
if (type === 'location' && objectId) {
const locationResponse = await fetch(`/api/locations/${objectId}/`);
if (locationResponse.ok) {
const updatedLocation = await locationResponse.json();
@ -589,20 +557,6 @@
} else {
console.error('Failed to refetch location data:', await locationResponse.text());
}
} else {
// Fallback: Update the visit's activities array locally
if (visits) {
visits = visits.map((visit) => {
if (visit.id === visitId) {
return {
...visit,
activities: (visit.activities || []).filter((a) => a.id !== activityId)
};
}
return visit;
});
}
}
} else {
console.error('Failed to delete activity:', await response.text());
alert('Failed to delete activity. Please try again.');
@ -661,14 +615,17 @@
}
}
function editVisit(visit: Visit | TransportationVisit) {
function editVisit(visit: Visit) {
isEditing = true;
visitIdEditing = visit.id;
const isAllDayEvent = isAllDay(visit.start_date);
allDay = isAllDayEvent;
if ('start_timezone' in visit) {
if ('start_timezone' in visit && typeof visit.start_timezone === 'string') {
selectedStartTimezone = visit.start_timezone;
if ('end_timezone' in visit && typeof visit.end_timezone === 'string') {
selectedEndTimezone = visit.end_timezone;
}
} else if (visit.timezone) {
selectedStartTimezone = visit.timezone;
}
@ -684,10 +641,14 @@
localEndDate = updateLocalDate({
utcDate: visit.end_date,
timezone: 'end_timezone' in visit ? visit.end_timezone : selectedStartTimezone
timezone:
'end_timezone' in visit && typeof visit.end_timezone === 'string'
? visit.end_timezone
: selectedStartTimezone
}).localDate;
}
// Remove the visit from the array temporarily for editing
if (visits) {
visits = visits.filter((v) => v.id !== visit.id);
}
@ -706,17 +667,6 @@
setTimeout(() => {
isEditing = false;
}, 0);
// Update the visits array in the parent component
if (type === 'location' && objectId) {
fetch(`/api/locations/${objectId}/`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ visits })
});
}
}
function removeVisit(visitId: string) {
@ -730,16 +680,17 @@
delete loadingActivities[visitId];
delete showActivityUpload[visitId];
// Patch updated visits array to location
if (type === 'location' && objectId) {
fetch(`/api/locations/${objectId}/`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ visits })
});
// make the DELETE request
fetch(`/api/visits/${visitId}/`, {
method: 'DELETE'
}).then((response) => {
if (!response.ok) {
alert('Failed to delete visit. Please try again.');
} else {
// remove the visit from the local state
visits = visits?.filter((v) => v.id !== visitId) ?? null;
}
});
}
function handleBack() {
@ -752,10 +703,6 @@
// Lifecycle
onMount(async () => {
if ((type === 'transportation' || type === 'lodging') && utcStartDate) {
allDay = isAllDay(utcStartDate);
}
localStartDate = updateLocalDate({
utcDate: utcStartDate,
timezone: selectedStartTimezone
@ -763,7 +710,7 @@
localEndDate = updateLocalDate({
utcDate: utcEndDate,
timezone: type === 'transportation' ? selectedEndTimezone : selectedStartTimezone
timezone: selectedEndTimezone
}).localDate;
if (!selectedStartTimezone) {
@ -787,8 +734,8 @@
}
});
$: typeConfig = getTypeConfig();
$: isDateValid = validateDateRange(utcStartDate ?? '', utcEndDate ?? '').valid;
$: typeConfig = getTypeConfig();
</script>
<div class="min-h-screen bg-gradient-to-br from-base-200/30 via-base-100 to-primary/5 p-6">
@ -814,35 +761,13 @@
<div class="space-y-4">
<!-- Timezone Selection -->
{#if type === 'transportation'}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="label-text text-sm font-medium" for="departure-timezone-selector"
>Departure Timezone</label
>
<label class="label-text text-sm font-medium" for="timezone-selector">Timezone</label>
<div class="mt-1">
<TimezoneSelector bind:selectedTimezone={selectedStartTimezone} />
</div>
</div>
<div>
<label class="label-text text-sm font-medium" for="arrival-timezone-selector"
>Arrival Timezone</label
>
<div class="mt-1">
<TimezoneSelector bind:selectedTimezone={selectedEndTimezone} />
</div>
</div>
</div>
{:else}
<div>
<label class="label-text text-sm font-medium" for="timezone-selector"
>Timezone</label
>
<div class="mt-1">
<TimezoneSelector bind:selectedTimezone={selectedStartTimezone} />
</div>
</div>
{/if}
<!-- Toggles -->
<div class="flex flex-wrap gap-6">
@ -941,7 +866,7 @@
</div>
<!-- Notes (Location only) -->
{#if type === 'location'}
<div class="mt-4">
<label class="label-text text-sm font-medium" for="visit-notes">Notes</label>
<textarea
@ -965,7 +890,6 @@
Add Visit
</button>
</div>
{/if}
</div>
<!-- Validation Error -->
@ -977,7 +901,7 @@
{/if}
<!-- Visits List (Location only) -->
{#if type === 'location'}
<div class="bg-base-50 p-4 rounded-lg border border-base-200">
<h3 class="font-medium text-base-content/80 mb-4">
Visits ({visits?.length || 0})
@ -1013,9 +937,9 @@
{visit.end_date && typeof visit.end_date === 'string'
? visit.end_date.split('T')[0]
: ''}
{:else if 'start_timezone' in visit}
{formatDateInTimezone(visit.start_date, visit.start_timezone)}
{formatDateInTimezone(visit.end_date, visit.end_timezone)}
{:else if 'start_timezone' in visit && visit.timezone}
{formatDateInTimezone(visit.start_date, visit.timezone)}
{formatDateInTimezone(visit.end_date, visit.timezone)}
{:else if visit.timezone}
{formatDateInTimezone(visit.start_date, visit.timezone)}
{formatDateInTimezone(visit.end_date, visit.timezone)}
@ -1113,8 +1037,7 @@
<div class="text-sm">
<div class="font-medium">Strava Activity Ready</div>
<div class="text-xs opacity-75">
GPX file downloaded. Please upload it below to complete the
import.
GPX file downloaded. Please upload it below to complete the import.
</div>
</div>
</div>
@ -1124,9 +1047,7 @@
<div class="bg-base-200/50 p-4 rounded-lg">
{#if pendingStravaImport[visit.id]}
<!-- Highlight GPX upload for Strava imports -->
<div
class="mb-6 p-4 bg-warning/10 border-2 border-warning/30 rounded-lg"
>
<div class="mb-6 p-4 bg-warning/10 border-2 border-warning/30 rounded-lg">
<div class="flex items-center gap-2 mb-2">
<FileIcon class="w-4 h-4 text-warning" />
<label
@ -1215,9 +1136,8 @@
<!-- Distance -->
<div>
<label
class="label-text text-xs font-medium"
for="distance-{visit.id}">Distance (km)</label
<label class="label-text text-xs font-medium" for="distance-{visit.id}"
>Distance (km)</label
>
<input
id="distance-{visit.id}"
@ -1311,9 +1231,8 @@
<!-- Calories -->
<div>
<label
class="label-text text-xs font-medium"
for="calories-{visit.id}">Calories</label
<label class="label-text text-xs font-medium" for="calories-{visit.id}"
>Calories</label
>
<input
id="calories-{visit.id}"
@ -1359,9 +1278,8 @@
<!-- Rest Time -->
<div>
<label
class="label-text text-xs font-medium"
for="rest-time-{visit.id}">Rest Time (s)</label
<label class="label-text text-xs font-medium" for="rest-time-{visit.id}"
>Rest Time (s)</label
>
<input
id="rest-time-{visit.id}"
@ -1375,9 +1293,8 @@
<!-- Start Latitude -->
<div>
<label
class="label-text text-xs font-medium"
for="start-lat-{visit.id}">Start Latitude</label
<label class="label-text text-xs font-medium" for="start-lat-{visit.id}"
>Start Latitude</label
>
<input
id="start-lat-{visit.id}"
@ -1392,9 +1309,8 @@
<!-- Start Longitude -->
<div>
<label
class="label-text text-xs font-medium"
for="start-lng-{visit.id}">Start Longitude</label
<label class="label-text text-xs font-medium" for="start-lng-{visit.id}"
>Start Longitude</label
>
<input
id="start-lng-{visit.id}"
@ -1441,9 +1357,8 @@
<!-- Timezone -->
<div>
<label
class="label-text text-xs font-medium"
for="timezone-{visit.id}">Timezone</label
<label class="label-text text-xs font-medium" for="timezone-{visit.id}"
>Timezone</label
>
<TimezoneSelector bind:selectedTimezone={activityForm.timezone} />
</div>
@ -1467,9 +1382,8 @@
<!-- Max Speed -->
<div>
<label
class="label-text text-xs font-medium"
for="max-speed-{visit.id}">Max Speed (m/s)</label
<label class="label-text text-xs font-medium" for="max-speed-{visit.id}"
>Max Speed (m/s)</label
>
<input
id="max-speed-{visit.id}"
@ -1500,7 +1414,7 @@
</div>
<!-- Trail Selection -->
{#if type === 'location' && trails && trails.length > 0}
{#if trails && trails.length > 0}
<div class="md:col-span-2">
<label
class="label-text text-xs font-medium"
@ -1638,7 +1552,6 @@
</div>
{/if}
</div>
{/if}
</div>
</div>

View file

@ -31,14 +31,7 @@ export type Location = {
rating?: number | null;
link?: string | null;
images: ContentImage[];
visits: {
id: string;
start_date: string;
end_date: string;
timezone: string | null;
notes: string;
activities: Activity[]; // Array of activities associated with the visit
}[];
visits: Visit[];
collections?: string[] | null;
latitude: number | null;
longitude: number | null;
@ -425,6 +418,9 @@ export type Visit = {
notes: string;
timezone: string | null;
activities?: Activity[];
location: string;
created_at: string;
updated_at: string;
};
export type TransportationVisit = {