mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-07-28 09:19:37 +02:00
Merge pull request #146 from seanmorley15/development
Trip Planner for Collections
This commit is contained in:
commit
03ca0770df
26 changed files with 1273 additions and 121 deletions
|
@ -1,7 +1,7 @@
|
||||||
import os
|
import os
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.utils.html import mark_safe
|
from django.utils.html import mark_safe
|
||||||
from .models import Adventure, Collection
|
from .models import Adventure, Collection, Transportation
|
||||||
from worldtravel.models import Country, Region, VisitedRegion
|
from worldtravel.models import Country, Region, VisitedRegion
|
||||||
|
|
||||||
|
|
||||||
|
@ -74,6 +74,7 @@ admin.site.register(Collection, CollectionAdmin)
|
||||||
admin.site.register(Country, CountryAdmin)
|
admin.site.register(Country, CountryAdmin)
|
||||||
admin.site.register(Region, RegionAdmin)
|
admin.site.register(Region, RegionAdmin)
|
||||||
admin.site.register(VisitedRegion)
|
admin.site.register(VisitedRegion)
|
||||||
|
admin.site.register(Transportation)
|
||||||
|
|
||||||
admin.site.site_header = 'AdventureLog Admin'
|
admin.site.site_header = 'AdventureLog Admin'
|
||||||
admin.site.site_title = 'AdventureLog Admin Site'
|
admin.site.site_title = 'AdventureLog Admin Site'
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Generated by Django 5.0.7 on 2024-07-27 18:18
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('adventures', '0011_adventure_updated_at'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='collection',
|
||||||
|
name='end_date',
|
||||||
|
field=models.DateField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='collection',
|
||||||
|
name='start_date',
|
||||||
|
field=models.DateField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Generated by Django 5.0.7 on 2024-07-27 22:49
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('adventures', '0012_collection_end_date_collection_start_date'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='adventure',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('visited', 'Visited'), ('planned', 'Planned'), ('lodging', 'Lodging'), ('dining', 'Dining')], max_length=100),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Transportation',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||||
|
('type', models.CharField(max_length=100)),
|
||||||
|
('name', models.CharField(max_length=200)),
|
||||||
|
('description', models.TextField(blank=True, null=True)),
|
||||||
|
('rating', models.FloatField(blank=True, null=True)),
|
||||||
|
('link', models.URLField(blank=True, null=True)),
|
||||||
|
('date', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('flight_number', models.CharField(blank=True, max_length=100, null=True)),
|
||||||
|
('from_location', models.CharField(blank=True, max_length=200, null=True)),
|
||||||
|
('to_location', models.CharField(blank=True, max_length=200, null=True)),
|
||||||
|
('is_public', models.BooleanField(default=False)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('collection', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='adventures.collection')),
|
||||||
|
('user_id', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
|
@ -8,6 +8,19 @@ from django_resized import ResizedImageField
|
||||||
ADVENTURE_TYPES = [
|
ADVENTURE_TYPES = [
|
||||||
('visited', 'Visited'),
|
('visited', 'Visited'),
|
||||||
('planned', 'Planned'),
|
('planned', 'Planned'),
|
||||||
|
('lodging', 'Lodging'),
|
||||||
|
('dining', 'Dining')
|
||||||
|
]
|
||||||
|
|
||||||
|
TRANSPORTATION_TYPES = [
|
||||||
|
('car', 'Car'),
|
||||||
|
('plane', 'Plane'),
|
||||||
|
('train', 'Train'),
|
||||||
|
('bus', 'Bus'),
|
||||||
|
('boat', 'Boat'),
|
||||||
|
('bike', 'Bike'),
|
||||||
|
('walking', 'Walking'),
|
||||||
|
('other', 'Other')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -56,6 +69,8 @@ class Collection(models.Model):
|
||||||
description = models.TextField(blank=True, null=True)
|
description = models.TextField(blank=True, null=True)
|
||||||
is_public = models.BooleanField(default=False)
|
is_public = models.BooleanField(default=False)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
start_date = models.DateField(blank=True, null=True)
|
||||||
|
end_date = models.DateField(blank=True, null=True)
|
||||||
|
|
||||||
# if connected adventures are private and collection is public, raise an error
|
# if connected adventures are private and collection is public, raise an error
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
@ -66,3 +81,33 @@ class Collection(models.Model):
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
# make a class for transportaiotn and make it linked to a collection. Make it so it can be used for different types of transportations like car, plane, train, etc.
|
||||||
|
|
||||||
|
class Transportation(models.Model):
|
||||||
|
id = models.AutoField(primary_key=True)
|
||||||
|
user_id = models.ForeignKey(
|
||||||
|
User, on_delete=models.CASCADE, default=default_user_id)
|
||||||
|
type = models.CharField(max_length=100, choices=TRANSPORTATION_TYPES)
|
||||||
|
name = models.CharField(max_length=200)
|
||||||
|
description = models.TextField(blank=True, null=True)
|
||||||
|
rating = models.FloatField(blank=True, null=True)
|
||||||
|
link = models.URLField(blank=True, null=True)
|
||||||
|
date = models.DateTimeField(blank=True, null=True)
|
||||||
|
flight_number = models.CharField(max_length=100, blank=True, null=True)
|
||||||
|
from_location = models.CharField(max_length=200, blank=True, null=True)
|
||||||
|
to_location = models.CharField(max_length=200, blank=True, null=True)
|
||||||
|
is_public = models.BooleanField(default=False)
|
||||||
|
collection = models.ForeignKey('Collection', on_delete=models.CASCADE, blank=True, null=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
if self.collection:
|
||||||
|
if self.collection.is_public and not self.is_public:
|
||||||
|
raise ValidationError('Transportations associated with a public collection must be public. Collection: ' + self.collection.name + ' Transportation: ' + self.name)
|
||||||
|
if self.user_id != self.collection.user_id:
|
||||||
|
raise ValidationError('Transportations must be associated with collections owned by the same user. Collection owner: ' + self.collection.user_id.username + ' Transportation owner: ' + self.user_id.username)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import os
|
import os
|
||||||
from .models import Adventure, Collection
|
from .models import Adventure, Collection, Transportation
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
class AdventureSerializer(serializers.ModelSerializer):
|
class AdventureSerializer(serializers.ModelSerializer):
|
||||||
|
@ -23,13 +23,49 @@ class AdventureSerializer(serializers.ModelSerializer):
|
||||||
return [activity.lower() for activity in value]
|
return [activity.lower() for activity in value]
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
class TransportationSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Transportation
|
||||||
|
fields = [
|
||||||
|
'id', 'user_id', 'type', 'name', 'description', 'rating',
|
||||||
|
'link', 'date', 'flight_number', 'from_location', 'to_location',
|
||||||
|
'is_public', 'collection', 'created_at', 'updated_at'
|
||||||
|
]
|
||||||
|
read_only_fields = ['id', 'created_at', 'updated_at']
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
# Check if the collection is public and the transportation is not
|
||||||
|
collection = data.get('collection')
|
||||||
|
is_public = data.get('is_public', False)
|
||||||
|
if collection and collection.is_public and not is_public:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
'Transportations associated with a public collection must be public.'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if the user owns the collection
|
||||||
|
request = self.context.get('request')
|
||||||
|
if request and collection and collection.user_id != request.user:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
'Transportations must be associated with collections owned by the same user.'
|
||||||
|
)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
# Set the user_id to the current user
|
||||||
|
validated_data['user_id'] = self.context['request'].user
|
||||||
|
return super().create(validated_data)
|
||||||
|
|
||||||
|
|
||||||
class CollectionSerializer(serializers.ModelSerializer):
|
class CollectionSerializer(serializers.ModelSerializer):
|
||||||
adventures = AdventureSerializer(many=True, read_only=True, source='adventure_set')
|
adventures = AdventureSerializer(many=True, read_only=True, source='adventure_set')
|
||||||
|
transportations = TransportationSerializer(many=True, read_only=True, source='transportation_set')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Collection
|
model = Collection
|
||||||
# fields are all plus the adventures field
|
# fields are all plus the adventures field
|
||||||
fields = ['id', 'description', 'user_id', 'name', 'is_public', 'adventures']
|
fields = ['id', 'description', 'user_id', 'name', 'is_public', 'adventures', 'created_at', 'start_date', 'end_date', 'transportations']
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
from .views import AdventureViewSet, CollectionViewSet, StatsViewSet, GenerateDescription, ActivityTypesView
|
from .views import AdventureViewSet, CollectionViewSet, StatsViewSet, GenerateDescription, ActivityTypesView, TransportationViewSet
|
||||||
|
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
router.register(r'adventures', AdventureViewSet, basename='adventures')
|
router.register(r'adventures', AdventureViewSet, basename='adventures')
|
||||||
|
@ -8,6 +8,7 @@ router.register(r'collections', CollectionViewSet, basename='collections')
|
||||||
router.register(r'stats', StatsViewSet, basename='stats')
|
router.register(r'stats', StatsViewSet, basename='stats')
|
||||||
router.register(r'generate', GenerateDescription, basename='generate')
|
router.register(r'generate', GenerateDescription, basename='generate')
|
||||||
router.register(r'activity-types', ActivityTypesView, basename='activity-types')
|
router.register(r'activity-types', ActivityTypesView, basename='activity-types')
|
||||||
|
router.register(r'transportations', TransportationViewSet, basename='transportations')
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
|
@ -4,9 +4,9 @@ from rest_framework.decorators import action
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from django.db.models.functions import Lower
|
from django.db.models.functions import Lower
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from .models import Adventure, Collection
|
from .models import Adventure, Collection, Transportation
|
||||||
from worldtravel.models import VisitedRegion, Region, Country
|
from worldtravel.models import VisitedRegion, Region, Country
|
||||||
from .serializers import AdventureSerializer, CollectionSerializer
|
from .serializers import AdventureSerializer, CollectionSerializer, TransportationSerializer
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from django.db.models import Q, Prefetch
|
from django.db.models import Q, Prefetch
|
||||||
from .permissions import IsOwnerOrReadOnly, IsPublicReadOnly
|
from .permissions import IsOwnerOrReadOnly, IsPublicReadOnly
|
||||||
|
@ -138,8 +138,9 @@ class AdventureViewSet(viewsets.ModelViewSet):
|
||||||
# queryset = Adventure.objects.filter(
|
# queryset = Adventure.objects.filter(
|
||||||
# Q(is_public=True) | Q(user_id=request.user.id), collection=None
|
# Q(is_public=True) | Q(user_id=request.user.id), collection=None
|
||||||
# )
|
# )
|
||||||
|
allowed_types = ['visited', 'planned']
|
||||||
queryset = Adventure.objects.filter(
|
queryset = Adventure.objects.filter(
|
||||||
Q(user_id=request.user.id)
|
Q(user_id=request.user.id) & Q(type__in=allowed_types)
|
||||||
)
|
)
|
||||||
|
|
||||||
queryset = self.apply_sorting(queryset)
|
queryset = self.apply_sorting(queryset)
|
||||||
|
@ -237,6 +238,9 @@ class CollectionViewSet(viewsets.ModelViewSet):
|
||||||
# Update associated adventures to match the collection's is_public status
|
# Update associated adventures to match the collection's is_public status
|
||||||
Adventure.objects.filter(collection=instance).update(is_public=new_public_status)
|
Adventure.objects.filter(collection=instance).update(is_public=new_public_status)
|
||||||
|
|
||||||
|
# do the same for transportations
|
||||||
|
Transportation.objects.filter(collection=instance).update(is_public=new_public_status)
|
||||||
|
|
||||||
# Log the action (optional)
|
# Log the action (optional)
|
||||||
action = "public" if new_public_status else "private"
|
action = "public" if new_public_status else "private"
|
||||||
print(f"Collection {instance.id} and its adventures were set to {action}")
|
print(f"Collection {instance.id} and its adventures were set to {action}")
|
||||||
|
@ -257,32 +261,16 @@ class CollectionViewSet(viewsets.ModelViewSet):
|
||||||
Prefetch('adventure_set', queryset=Adventure.objects.filter(
|
Prefetch('adventure_set', queryset=Adventure.objects.filter(
|
||||||
Q(is_public=True) | Q(user_id=self.request.user.id)
|
Q(is_public=True) | Q(user_id=self.request.user.id)
|
||||||
))
|
))
|
||||||
|
).prefetch_related(
|
||||||
|
Prefetch('transportation_set', queryset=Transportation.objects.filter(
|
||||||
|
Q(is_public=True) | Q(user_id=self.request.user.id)
|
||||||
|
))
|
||||||
)
|
)
|
||||||
return self.apply_sorting(collections)
|
return self.apply_sorting(collections)
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
serializer.save(user_id=self.request.user)
|
serializer.save(user_id=self.request.user)
|
||||||
|
|
||||||
# @action(detail=False, methods=['get'])
|
|
||||||
# def filtered(self, request):
|
|
||||||
# types = request.query_params.get('types', '').split(',')
|
|
||||||
# valid_types = ['visited', 'planned']
|
|
||||||
# types = [t for t in types if t in valid_types]
|
|
||||||
|
|
||||||
# if not types:
|
|
||||||
# return Response({"error": "No valid types provided"}, status=400)
|
|
||||||
|
|
||||||
# queryset = Collection.objects.none()
|
|
||||||
|
|
||||||
# for adventure_type in types:
|
|
||||||
# if adventure_type in ['visited', 'planned']:
|
|
||||||
# queryset |= Collection.objects.filter(
|
|
||||||
# type=adventure_type, user_id=request.user.id)
|
|
||||||
|
|
||||||
# queryset = self.apply_sorting(queryset)
|
|
||||||
# collections = self.paginate_and_respond(queryset, request)
|
|
||||||
# return collections
|
|
||||||
|
|
||||||
def paginate_and_respond(self, queryset, request):
|
def paginate_and_respond(self, queryset, request):
|
||||||
paginator = self.pagination_class()
|
paginator = self.pagination_class()
|
||||||
page = paginator.paginate_queryset(queryset, request)
|
page = paginator.paginate_queryset(queryset, request)
|
||||||
|
@ -378,3 +366,40 @@ class ActivityTypesView(viewsets.ViewSet):
|
||||||
allTypes.append(x)
|
allTypes.append(x)
|
||||||
|
|
||||||
return Response(allTypes)
|
return Response(allTypes)
|
||||||
|
|
||||||
|
class TransportationViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = Transportation.objects.all()
|
||||||
|
serializer_class = TransportationSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
filterset_fields = ['type', 'is_public', 'collection']
|
||||||
|
|
||||||
|
# return error message if user is not authenticated on the root endpoint
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
# Prevent listing all adventures
|
||||||
|
return Response({"detail": "Listing all adventures is not allowed."},
|
||||||
|
status=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
@action(detail=False, methods=['get'])
|
||||||
|
def all(self, request):
|
||||||
|
if not request.user.is_authenticated:
|
||||||
|
return Response({"error": "User is not authenticated"}, status=400)
|
||||||
|
queryset = Transportation.objects.filter(
|
||||||
|
Q(user_id=request.user.id)
|
||||||
|
)
|
||||||
|
serializer = self.get_serializer(queryset, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
|
||||||
|
"""
|
||||||
|
This view should return a list of all transportations
|
||||||
|
for the currently authenticated user.
|
||||||
|
"""
|
||||||
|
user = self.request.user
|
||||||
|
return Transportation.objects.filter(user_id=user)
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
serializer.save(user_id=self.request.user)
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
from .views import CountryViewSet, RegionViewSet, VisitedRegionViewSet, regions_by_country, visits_by_country
|
from .views import CountryViewSet, RegionViewSet, VisitedRegionViewSet, regions_by_country, visits_by_country, GeoJSONView
|
||||||
|
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
router.register(r'countries', CountryViewSet, basename='countries')
|
router.register(r'countries', CountryViewSet, basename='countries')
|
||||||
|
@ -13,4 +13,5 @@ urlpatterns = [
|
||||||
path('', include(router.urls)),
|
path('', include(router.urls)),
|
||||||
path('<str:country_code>/regions/', regions_by_country, name='regions-by-country'),
|
path('<str:country_code>/regions/', regions_by_country, name='regions-by-country'),
|
||||||
path('<str:country_code>/visits/', visits_by_country, name='visits-by-country'),
|
path('<str:country_code>/visits/', visits_by_country, name='visits-by-country'),
|
||||||
|
path('geojson/', GeoJSONView.as_view({'get': 'list'}), name='geojson'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,6 +6,10 @@ from rest_framework.permissions import IsAuthenticated
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.decorators import api_view, permission_classes
|
from rest_framework.decorators import api_view, permission_classes
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.staticfiles import finders
|
||||||
|
|
||||||
@api_view(['GET'])
|
@api_view(['GET'])
|
||||||
@permission_classes([IsAuthenticated])
|
@permission_classes([IsAuthenticated])
|
||||||
|
@ -50,3 +54,37 @@ class VisitedRegionViewSet(viewsets.ModelViewSet):
|
||||||
self.perform_create(serializer)
|
self.perform_create(serializer)
|
||||||
headers = self.get_success_headers(serializer.data)
|
headers = self.get_success_headers(serializer.data)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||||
|
|
||||||
|
class GeoJSONView(viewsets.ViewSet):
|
||||||
|
"""
|
||||||
|
Combine all GeoJSON data from .json files in static/data into a single GeoJSON object.
|
||||||
|
"""
|
||||||
|
def list(self, request):
|
||||||
|
combined_geojson = {
|
||||||
|
"type": "FeatureCollection",
|
||||||
|
"features": []
|
||||||
|
}
|
||||||
|
|
||||||
|
# Use Django's static file finder to locate the 'data' directory
|
||||||
|
data_dir = finders.find('data')
|
||||||
|
|
||||||
|
if not data_dir or not os.path.isdir(data_dir):
|
||||||
|
return Response({"error": "Data directory does not exist."}, status=404)
|
||||||
|
|
||||||
|
for filename in os.listdir(data_dir):
|
||||||
|
if filename.endswith('.json'):
|
||||||
|
file_path = os.path.join(data_dir, filename)
|
||||||
|
try:
|
||||||
|
with open(file_path, 'r') as f:
|
||||||
|
json_data = json.load(f)
|
||||||
|
# Check if the JSON data is GeoJSON
|
||||||
|
if isinstance(json_data, dict) and "type" in json_data:
|
||||||
|
if json_data["type"] == "FeatureCollection":
|
||||||
|
combined_geojson["features"].extend(json_data.get("features", []))
|
||||||
|
elif json_data["type"] == "Feature":
|
||||||
|
combined_geojson["features"].append(json_data)
|
||||||
|
# You can add more conditions here for other GeoJSON types if needed
|
||||||
|
except (IOError, json.JSONDecodeError) as e:
|
||||||
|
return Response({"error": f"Error reading file {filename}: {str(e)}"}, status=500)
|
||||||
|
|
||||||
|
return Response(combined_geojson)
|
|
@ -24,8 +24,20 @@
|
||||||
|
|
||||||
let isCollectionModalOpen: boolean = false;
|
let isCollectionModalOpen: boolean = false;
|
||||||
|
|
||||||
|
let keyword: string = '';
|
||||||
|
|
||||||
export let adventure: Adventure;
|
export let adventure: Adventure;
|
||||||
|
|
||||||
|
if (adventure.type == 'visited') {
|
||||||
|
keyword = 'Adventure';
|
||||||
|
} else if (adventure.type == 'planned') {
|
||||||
|
keyword = 'Adventure';
|
||||||
|
} else if (adventure.type == 'lodging') {
|
||||||
|
keyword = 'Lodging';
|
||||||
|
} else if (adventure.type == 'dining') {
|
||||||
|
keyword = 'Dining';
|
||||||
|
}
|
||||||
|
|
||||||
let activityTypes: string[] = [];
|
let activityTypes: string[] = [];
|
||||||
// makes it reactivty to changes so it updates automatically
|
// makes it reactivty to changes so it updates automatically
|
||||||
$: {
|
$: {
|
||||||
|
@ -149,9 +161,16 @@
|
||||||
<div>
|
<div>
|
||||||
{#if adventure.type == 'visited' && user?.pk == adventure.user_id}
|
{#if adventure.type == 'visited' && user?.pk == adventure.user_id}
|
||||||
<div class="badge badge-primary">Visited</div>
|
<div class="badge badge-primary">Visited</div>
|
||||||
{:else if user?.pk == adventure.user_id}
|
{:else if user?.pk == adventure.user_id && adventure.type == 'planned'}
|
||||||
<div class="badge badge-secondary">Planned</div>
|
<div class="badge badge-secondary">Planned</div>
|
||||||
|
{:else if (user?.pk !== adventure.user_id && adventure.type == 'planned') || adventure.type == 'visited'}
|
||||||
|
<div class="badge badge-secondary">Adventure</div>
|
||||||
|
{:else if user?.pk == adventure.user_id && adventure.type == 'lodging'}
|
||||||
|
<div class="badge badge-success">Lodging</div>
|
||||||
|
{:else if adventure.type == 'dining'}
|
||||||
|
<div class="badge badge-accent">Dining</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="badge badge-neutral">{adventure.is_public ? 'Public' : 'Private'}</div>
|
<div class="badge badge-neutral">{adventure.is_public ? 'Public' : 'Private'}</div>
|
||||||
</div>
|
</div>
|
||||||
{#if adventure.location && adventure.location !== ''}
|
{#if adventure.location && adventure.location !== ''}
|
||||||
|
@ -163,7 +182,7 @@
|
||||||
{#if adventure.date && adventure.date !== ''}
|
{#if adventure.date && adventure.date !== ''}
|
||||||
<div class="inline-flex items-center">
|
<div class="inline-flex items-center">
|
||||||
<Calendar class="w-5 h-5 mr-1" />
|
<Calendar class="w-5 h-5 mr-1" />
|
||||||
<p>{new Date(adventure.date).toLocaleDateString()}</p>
|
<p>{new Date(adventure.date).toLocaleDateString('en-US', { timeZone: 'UTC' })}</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if adventure.activity_types && adventure.activity_types.length > 0}
|
{#if adventure.activity_types && adventure.activity_types.length > 0}
|
||||||
|
@ -194,7 +213,7 @@
|
||||||
><Launch class="w-6 h-6" />Open Details</button
|
><Launch class="w-6 h-6" />Open Details</button
|
||||||
>
|
>
|
||||||
<button class="btn btn-neutral mb-2" on:click={editAdventure}>
|
<button class="btn btn-neutral mb-2" on:click={editAdventure}>
|
||||||
<FileDocumentEdit class="w-6 h-6" />Edit Adventure
|
<FileDocumentEdit class="w-6 h-6" />Edit {keyword}
|
||||||
</button>
|
</button>
|
||||||
{#if adventure.type == 'visited'}
|
{#if adventure.type == 'visited'}
|
||||||
<button class="btn btn-neutral mb-2" on:click={changeType('planned')}
|
<button class="btn btn-neutral mb-2" on:click={changeType('planned')}
|
||||||
|
@ -206,11 +225,18 @@
|
||||||
><CheckBold class="w-6 h-6" />Mark Visited</button
|
><CheckBold class="w-6 h-6" />Mark Visited</button
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
{#if adventure.collection}
|
<!-- remove from adventure -->
|
||||||
|
{#if (adventure.collection && adventure.type == 'visited') || adventure.type == 'planned'}
|
||||||
<button class="btn btn-neutral mb-2" on:click={removeFromCollection}
|
<button class="btn btn-neutral mb-2" on:click={removeFromCollection}
|
||||||
><LinkVariantRemove class="w-6 h-6" />Remove from Collection</button
|
><LinkVariantRemove class="w-6 h-6" />Remove from Collection</button
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
|
<!-- change a non adventure to an adventure -->
|
||||||
|
{#if (adventure.collection && adventure.type == 'lodging') || adventure.type == 'dining'}
|
||||||
|
<button class="btn btn-neutral mb-2" on:click={changeType('visited')}
|
||||||
|
><CheckBold class="w-6 h-6" />Change to Visit</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
{#if !adventure.collection}
|
{#if !adventure.collection}
|
||||||
<button class="btn btn-neutral mb-2" on:click={() => (isCollectionModalOpen = true)}
|
<button class="btn btn-neutral mb-2" on:click={() => (isCollectionModalOpen = true)}
|
||||||
><Plus class="w-6 h-6" />Add to Collection</button
|
><Plus class="w-6 h-6" />Add to Collection</button
|
||||||
|
|
|
@ -47,6 +47,20 @@
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="card-title overflow-ellipsis">{collection.name}</h2>
|
<h2 class="card-title overflow-ellipsis">{collection.name}</h2>
|
||||||
<p>{collection.adventures.length} Adventures</p>
|
<p>{collection.adventures.length} Adventures</p>
|
||||||
|
{#if collection.start_date && collection.end_date}
|
||||||
|
<p>
|
||||||
|
Dates: {new Date(collection.start_date).toLocaleDateString('en-US', { timeZone: 'UTC' })} - {new Date(
|
||||||
|
collection.end_date
|
||||||
|
).toLocaleDateString('en-US', { timeZone: 'UTC' })}
|
||||||
|
</p>
|
||||||
|
<!-- display the duration in days -->
|
||||||
|
<p>
|
||||||
|
Duration: {Math.floor(
|
||||||
|
(new Date(collection.end_date).getTime() - new Date(collection.start_date).getTime()) /
|
||||||
|
(1000 * 60 * 60 * 24)
|
||||||
|
) + 1}{' '}
|
||||||
|
days
|
||||||
|
</p>{/if}
|
||||||
<div class="card-actions justify-end">
|
<div class="card-actions justify-end">
|
||||||
{#if type != 'link'}
|
{#if type != 'link'}
|
||||||
<button on:click={deleteCollection} class="btn btn-secondary"
|
<button on:click={deleteCollection} class="btn btn-secondary"
|
||||||
|
|
|
@ -222,20 +222,22 @@
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
{#if adventureToEdit.type == 'visited' || adventureToEdit.type == 'planned'}
|
||||||
<label for="activityTypes"
|
<div class="mb-2">
|
||||||
>Activity Types <ClipboardList class="inline-block -mt-1 mb-1 w-6 h-6" /></label
|
<label for="activityTypes"
|
||||||
><br />
|
>Activity Types <ClipboardList class="inline-block -mt-1 mb-1 w-6 h-6" /></label
|
||||||
<input
|
><br />
|
||||||
type="text"
|
<input
|
||||||
id="activity_types"
|
type="text"
|
||||||
name="activity_types"
|
id="activity_types"
|
||||||
hidden
|
name="activity_types"
|
||||||
bind:value={adventureToEdit.activity_types}
|
hidden
|
||||||
class="input input-bordered w-full max-w-xs mt-1"
|
bind:value={adventureToEdit.activity_types}
|
||||||
/>
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
<ActivityComplete bind:activities={adventureToEdit.activity_types} />
|
/>
|
||||||
</div>
|
<ActivityComplete bind:activities={adventureToEdit.activity_types} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label for="image">Image </label><br />
|
<label for="image">Image </label><br />
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
|
|
|
@ -109,6 +109,7 @@
|
||||||
bind:value={collectionToEdit.description}
|
bind:value={collectionToEdit.description}
|
||||||
class="input input-bordered w-full max-w-xs mt-1 mb-2"
|
class="input input-bordered w-full max-w-xs mt-1 mb-2"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- <button
|
<!-- <button
|
||||||
class="btn btn-neutral ml-2"
|
class="btn btn-neutral ml-2"
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -117,6 +118,28 @@
|
||||||
></iconify-icon>Generate Description</button
|
></iconify-icon>Generate Description</button
|
||||||
> -->
|
> -->
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="start_date">Start Date <Calendar class="inline-block mb-1 w-6 h-6" /></label
|
||||||
|
><br />
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
id="start_date"
|
||||||
|
name="start_date"
|
||||||
|
bind:value={collectionToEdit.start_date}
|
||||||
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="end_date">End Date <Calendar class="inline-block mb-1 w-6 h-6" /></label><br
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
id="end_date"
|
||||||
|
name="end_date"
|
||||||
|
bind:value={collectionToEdit.end_date}
|
||||||
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label for="is_public">Public <Earth class="inline-block -mt-1 mb-1 w-6 h-6" /></label><br
|
<label for="is_public">Public <Earth class="inline-block -mt-1 mb-1 w-6 h-6" /></label><br
|
||||||
|
|
217
frontend/src/lib/components/EditTransportation.svelte
Normal file
217
frontend/src/lib/components/EditTransportation.svelte
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
<script lang="ts">
|
||||||
|
export let transportationToEdit: Transportation;
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import type { Transportation } from '$lib/types';
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { addToast } from '$lib/toasts';
|
||||||
|
let modal: HTMLDialogElement;
|
||||||
|
|
||||||
|
console.log(transportationToEdit.id);
|
||||||
|
|
||||||
|
let originalName = transportationToEdit.name;
|
||||||
|
|
||||||
|
import MapMarker from '~icons/mdi/map-marker';
|
||||||
|
import Calendar from '~icons/mdi/calendar';
|
||||||
|
import Notebook from '~icons/mdi/notebook';
|
||||||
|
import Star from '~icons/mdi/star';
|
||||||
|
import PlaneCar from '~icons/mdi/plane-car';
|
||||||
|
import LinkVariant from '~icons/mdi/link-variant';
|
||||||
|
import Airplane from '~icons/mdi/airplane';
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
|
||||||
|
if (modal) {
|
||||||
|
modal.showModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (transportationToEdit.date) {
|
||||||
|
transportationToEdit.date = transportationToEdit.date.slice(0, 19);
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
dispatch('close');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit(event: Event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const form = event.target as HTMLFormElement;
|
||||||
|
const formData = new FormData(form);
|
||||||
|
|
||||||
|
const response = await fetch(`/api/transportations/${transportationToEdit.id}/`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
transportationToEdit = result;
|
||||||
|
|
||||||
|
addToast('success', 'Transportation edited successfully!');
|
||||||
|
dispatch('saveEdit', transportationToEdit);
|
||||||
|
close();
|
||||||
|
} else {
|
||||||
|
addToast('error', 'Error editing transportaion');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<dialog id="my_modal_1" class="modal">
|
||||||
|
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||||
|
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||||
|
<div class="modal-box" role="dialog" on:keydown={handleKeydown} tabindex="0">
|
||||||
|
<h3 class="font-bold text-lg">Edit Transportation: {originalName}</h3>
|
||||||
|
<div
|
||||||
|
class="modal-action items-center"
|
||||||
|
style="display: flex; flex-direction: column; align-items: center; width: 100%;"
|
||||||
|
>
|
||||||
|
<form method="post" style="width: 100%;" on:submit={handleSubmit}>
|
||||||
|
<div class="mb-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="id"
|
||||||
|
name="id"
|
||||||
|
hidden
|
||||||
|
readonly
|
||||||
|
bind:value={transportationToEdit.id}
|
||||||
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="is_public"
|
||||||
|
name="is_public"
|
||||||
|
hidden
|
||||||
|
readonly
|
||||||
|
bind:value={transportationToEdit.is_public}
|
||||||
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
|
/>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="type">Type <PlaneCar class="inline-block mb-1 w-6 h-6" /></label><br />
|
||||||
|
<select
|
||||||
|
class="select select-bordered w-full max-w-xs"
|
||||||
|
name="type"
|
||||||
|
id="type"
|
||||||
|
bind:value={transportationToEdit.type}
|
||||||
|
>
|
||||||
|
<option disabled selected>Transport Type</option>
|
||||||
|
<option value="car">Car</option>
|
||||||
|
<option value="plane">Plane</option>
|
||||||
|
<option value="train">Train</option>
|
||||||
|
<option value="bus">Bus</option>
|
||||||
|
<option value="boat">Boat</option>
|
||||||
|
<option value="bike">Bike</option>
|
||||||
|
<option value="walking">Walking</option>
|
||||||
|
<option value="other">Other</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label for="name">Name</label><br />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
id="name"
|
||||||
|
bind:value={transportationToEdit.name}
|
||||||
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="date">Description <Notebook class="inline-block -mt-1 mb-1 w-6 h-6" /></label
|
||||||
|
><br />
|
||||||
|
<div class="flex">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="description"
|
||||||
|
name="description"
|
||||||
|
bind:value={transportationToEdit.description}
|
||||||
|
class="input input-bordered w-full max-w-xs mt-1 mb-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="start_date"
|
||||||
|
>Date & Time <Calendar class="inline-block mb-1 w-6 h-6" /></label
|
||||||
|
><br />
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
id="date"
|
||||||
|
name="date"
|
||||||
|
bind:value={transportationToEdit.date}
|
||||||
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="rating">Rating <Star class="inline-block mb-1 w-6 h-6" /></label><br />
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
max="5"
|
||||||
|
min="0"
|
||||||
|
id="rating"
|
||||||
|
name="rating"
|
||||||
|
bind:value={transportationToEdit.rating}
|
||||||
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="rating">Link <LinkVariant class="inline-block mb-1 w-6 h-6" /></label><br />
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
id="link"
|
||||||
|
name="link"
|
||||||
|
bind:value={transportationToEdit.link}
|
||||||
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if transportationToEdit.type == 'plane'}
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="flight_number"
|
||||||
|
>Flight Number <Airplane class="inline-block mb-1 w-6 h-6" /></label
|
||||||
|
><br />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="flight_number"
|
||||||
|
name="flight_number"
|
||||||
|
bind:value={transportationToEdit.flight_number}
|
||||||
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="rating">From Location <MapMarker class="inline-block mb-1 w-6 h-6" /></label
|
||||||
|
><br />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="from_location"
|
||||||
|
name="from_location"
|
||||||
|
bind:value={transportationToEdit.from_location}
|
||||||
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="rating">To Location <MapMarker class="inline-block mb-1 w-6 h-6" /></label
|
||||||
|
><br />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="to_location"
|
||||||
|
name="to_location"
|
||||||
|
bind:value={transportationToEdit.to_location}
|
||||||
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary mr-4 mt-4">Edit</button>
|
||||||
|
<!-- if there is a button in form, it will close the modal -->
|
||||||
|
<button class="btn mt-4" on:click={close}>Close</button>
|
||||||
|
</form>
|
||||||
|
<div class="flex items-center justify-center flex-wrap gap-4 mt-4"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
|
@ -11,6 +11,7 @@
|
||||||
|
|
||||||
export let longitude: number | null = null;
|
export let longitude: number | null = null;
|
||||||
export let latitude: number | null = null;
|
export let latitude: number | null = null;
|
||||||
|
export let collection_id: number | null = null;
|
||||||
|
|
||||||
import MapMarker from '~icons/mdi/map-marker';
|
import MapMarker from '~icons/mdi/map-marker';
|
||||||
import Calendar from '~icons/mdi/calendar';
|
import Calendar from '~icons/mdi/calendar';
|
||||||
|
@ -23,6 +24,7 @@
|
||||||
import Wikipedia from '~icons/mdi/wikipedia';
|
import Wikipedia from '~icons/mdi/wikipedia';
|
||||||
import ActivityComplete from './ActivityComplete.svelte';
|
import ActivityComplete from './ActivityComplete.svelte';
|
||||||
import { appVersion } from '$lib/config';
|
import { appVersion } from '$lib/config';
|
||||||
|
import AdventureCard from './AdventureCard.svelte';
|
||||||
|
|
||||||
let newAdventure: Adventure = {
|
let newAdventure: Adventure = {
|
||||||
id: NaN,
|
id: NaN,
|
||||||
|
@ -39,7 +41,7 @@
|
||||||
latitude: null,
|
latitude: null,
|
||||||
longitude: null,
|
longitude: null,
|
||||||
is_public: false,
|
is_public: false,
|
||||||
collection: null
|
collection: collection_id || NaN
|
||||||
};
|
};
|
||||||
|
|
||||||
if (longitude && latitude) {
|
if (longitude && latitude) {
|
||||||
|
@ -293,20 +295,22 @@
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
{#if newAdventure.type == 'visited' || newAdventure.type == 'planned'}
|
||||||
<label for="activityTypes"
|
<div class="mb-2">
|
||||||
>Activity Types <ClipboardList class="inline-block -mt-1 mb-1 w-6 h-6" /></label
|
<label for="activityTypes"
|
||||||
><br />
|
>Activity Types <ClipboardList class="inline-block -mt-1 mb-1 w-6 h-6" /></label
|
||||||
<input
|
><br />
|
||||||
type="text"
|
<input
|
||||||
id="activity_types"
|
type="text"
|
||||||
name="activity_types"
|
id="activity_types"
|
||||||
hidden
|
name="activity_types"
|
||||||
bind:value={newAdventure.activity_types}
|
hidden
|
||||||
class="input input-bordered w-full max-w-xs mt-1"
|
bind:value={newAdventure.activity_types}
|
||||||
/>
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
<ActivityComplete bind:activities={newAdventure.activity_types} />
|
/>
|
||||||
</div>
|
<ActivityComplete bind:activities={newAdventure.activity_types} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label for="rating"
|
<label for="rating"
|
||||||
>Rating <iconify-icon icon="mdi:star" class="text-xl -mb-1"></iconify-icon></label
|
>Rating <iconify-icon icon="mdi:star" class="text-xl -mb-1"></iconify-icon></label
|
||||||
|
@ -371,6 +375,14 @@
|
||||||
bind:value={newAdventure.longitude}
|
bind:value={newAdventure.longitude}
|
||||||
class="input input-bordered w-full max-w-xs mt-1"
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
/>
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="collection"
|
||||||
|
name="collection"
|
||||||
|
hidden
|
||||||
|
bind:value={newAdventure.collection}
|
||||||
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
|
/>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary mr-4 mt-4">Create</button>
|
<button type="submit" class="btn btn-primary mr-4 mt-4">Create</button>
|
||||||
<button type="button" class="btn mt-4" on:click={close}>Close</button>
|
<button type="button" class="btn mt-4" on:click={close}>Close</button>
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import { addToast } from '$lib/toasts';
|
import { addToast } from '$lib/toasts';
|
||||||
|
|
||||||
|
import Calendar from '~icons/mdi/calendar';
|
||||||
|
|
||||||
let newCollection: Collection = {
|
let newCollection: Collection = {
|
||||||
user_id: NaN,
|
user_id: NaN,
|
||||||
id: NaN,
|
id: NaN,
|
||||||
|
@ -104,10 +106,32 @@
|
||||||
class="input input-bordered w-full max-w-xs mt-1 mb-2"
|
class="input input-bordered w-full max-w-xs mt-1 mb-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="mb-2">
|
||||||
<div class="mb-2">
|
<label for="start_date">Start Date <Calendar class="inline-block mb-1 w-6 h-6" /></label
|
||||||
<button type="submit" class="btn btn-primary mr-4 mt-4">Create</button>
|
><br />
|
||||||
<button type="button" class="btn mt-4" on:click={close}>Close</button>
|
<input
|
||||||
|
type="date"
|
||||||
|
id="start_date"
|
||||||
|
name="start_date"
|
||||||
|
bind:value={newCollection.start_date}
|
||||||
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="end_date">End Date <Calendar class="inline-block mb-1 w-6 h-6" /></label><br
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
id="end_date"
|
||||||
|
name="end_date"
|
||||||
|
bind:value={newCollection.end_date}
|
||||||
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<button type="submit" class="btn btn-primary mr-4 mt-4">Create</button>
|
||||||
|
<button type="button" class="btn mt-4" on:click={close}>Close</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
232
frontend/src/lib/components/NewTransportation.svelte
Normal file
232
frontend/src/lib/components/NewTransportation.svelte
Normal file
|
@ -0,0 +1,232 @@
|
||||||
|
<script lang="ts">
|
||||||
|
// let newTransportation: Transportation = {
|
||||||
|
// id:NaN,
|
||||||
|
// user_id: NaN,
|
||||||
|
// type: '',
|
||||||
|
// name: '',
|
||||||
|
// description: null,
|
||||||
|
// rating: NaN,
|
||||||
|
// link: null,
|
||||||
|
// date: null,
|
||||||
|
// flight_number: null,
|
||||||
|
// from_location: null,
|
||||||
|
// to_location: null,
|
||||||
|
// is_public: false,
|
||||||
|
// collection: null,
|
||||||
|
// created_at: '',
|
||||||
|
// updated_at: ''
|
||||||
|
// };
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import type { Collection, Transportation } from '$lib/types';
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { addToast } from '$lib/toasts';
|
||||||
|
let modal: HTMLDialogElement;
|
||||||
|
|
||||||
|
export let collection: Collection;
|
||||||
|
|
||||||
|
import MapMarker from '~icons/mdi/map-marker';
|
||||||
|
import Calendar from '~icons/mdi/calendar';
|
||||||
|
import Notebook from '~icons/mdi/notebook';
|
||||||
|
import Star from '~icons/mdi/star';
|
||||||
|
import PlaneCar from '~icons/mdi/plane-car';
|
||||||
|
import LinkVariant from '~icons/mdi/link-variant';
|
||||||
|
import Airplane from '~icons/mdi/airplane';
|
||||||
|
|
||||||
|
let type: string = '';
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
|
||||||
|
if (modal) {
|
||||||
|
modal.showModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// if (newTransportation.date) {
|
||||||
|
// newTransportation.date = newTransportation.date.slice(0, 19);
|
||||||
|
// }
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
dispatch('close');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit(event: Event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const form = event.target as HTMLFormElement;
|
||||||
|
const formData = new FormData(form);
|
||||||
|
|
||||||
|
const response = await fetch(`/api/transportations/`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
addToast('success', 'Transportation added successfully!');
|
||||||
|
dispatch('add', result);
|
||||||
|
close();
|
||||||
|
} else {
|
||||||
|
addToast('error', 'Error editing transportation');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<dialog id="my_modal_1" class="modal">
|
||||||
|
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||||
|
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||||
|
<div class="modal-box" role="dialog" on:keydown={handleKeydown} tabindex="0">
|
||||||
|
<h3 class="font-bold text-lg">New Transportation</h3>
|
||||||
|
<div
|
||||||
|
class="modal-action items-center"
|
||||||
|
style="display: flex; flex-direction: column; align-items: center; width: 100%;"
|
||||||
|
>
|
||||||
|
<form method="post" style="width: 100%;" on:submit={handleSubmit}>
|
||||||
|
<div class="mb-2">
|
||||||
|
<!-- <input
|
||||||
|
type="text"
|
||||||
|
id="id"
|
||||||
|
name="id"
|
||||||
|
hidden
|
||||||
|
readonly
|
||||||
|
bind:value={newTransportation.id}
|
||||||
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
|
/> -->
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="collection"
|
||||||
|
name="collection"
|
||||||
|
hidden
|
||||||
|
readonly
|
||||||
|
bind:value={collection.id}
|
||||||
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="is_public"
|
||||||
|
name="is_public"
|
||||||
|
hidden
|
||||||
|
readonly
|
||||||
|
bind:value={collection.is_public}
|
||||||
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
|
/>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="type">Type <PlaneCar class="inline-block mb-1 w-6 h-6" /></label><br />
|
||||||
|
<select
|
||||||
|
class="select select-bordered w-full max-w-xs"
|
||||||
|
name="type"
|
||||||
|
id="type"
|
||||||
|
bind:value={type}
|
||||||
|
>
|
||||||
|
<option disabled selected>Transport Type</option>
|
||||||
|
<option value="car">Car</option>
|
||||||
|
<option value="plane">Plane</option>
|
||||||
|
<option value="train">Train</option>
|
||||||
|
<option value="bus">Bus</option>
|
||||||
|
<option value="boat">Boat</option>
|
||||||
|
<option value="bike">Bike</option>
|
||||||
|
<option value="walking">Walking</option>
|
||||||
|
<option value="other">Other</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label for="name">Name</label><br />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
id="name"
|
||||||
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="date">Description <Notebook class="inline-block -mt-1 mb-1 w-6 h-6" /></label
|
||||||
|
><br />
|
||||||
|
<div class="flex">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="description"
|
||||||
|
name="description"
|
||||||
|
class="input input-bordered w-full max-w-xs mt-1 mb-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="start_date"
|
||||||
|
>Date & Time <Calendar class="inline-block mb-1 w-6 h-6" /></label
|
||||||
|
><br />
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
id="date"
|
||||||
|
name="date"
|
||||||
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="rating">Rating <Star class="inline-block mb-1 w-6 h-6" /></label><br />
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
max="5"
|
||||||
|
min="0"
|
||||||
|
id="rating"
|
||||||
|
name="rating"
|
||||||
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="rating">Link <LinkVariant class="inline-block mb-1 w-6 h-6" /></label><br />
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
id="link"
|
||||||
|
name="link"
|
||||||
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if type == 'plane'}
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="flight_number"
|
||||||
|
>Flight Number <Airplane class="inline-block mb-1 w-6 h-6" /></label
|
||||||
|
><br />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="flight_number"
|
||||||
|
name="flight_number"
|
||||||
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="rating">From Location <MapMarker class="inline-block mb-1 w-6 h-6" /></label
|
||||||
|
><br />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="from_location"
|
||||||
|
name="from_location"
|
||||||
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="rating">To Location <MapMarker class="inline-block mb-1 w-6 h-6" /></label
|
||||||
|
><br />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="to_location"
|
||||||
|
name="to_location"
|
||||||
|
class="input input-bordered w-full max-w-xs mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary mr-4 mt-4">Edit</button>
|
||||||
|
<!-- if there is a button in form, it will close the modal -->
|
||||||
|
<button class="btn mt-4" on:click={close}>Close</button>
|
||||||
|
</form>
|
||||||
|
<div class="flex items-center justify-center flex-wrap gap-4 mt-4"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
|
@ -85,7 +85,9 @@
|
||||||
if (!adventure.name) {
|
if (!adventure.name) {
|
||||||
adventure.name = markers[0].name;
|
adventure.name = markers[0].name;
|
||||||
}
|
}
|
||||||
adventure.activity_types = [...adventure.activity_types, markers[0].activity_type];
|
if (adventure.type == 'visited' || adventure.type == 'planned') {
|
||||||
|
adventure.activity_types = [...adventure.activity_types, markers[0].activity_type];
|
||||||
|
}
|
||||||
dispatch('submit', adventure);
|
dispatch('submit', adventure);
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
|
|
63
frontend/src/lib/components/TransportationCard.svelte
Normal file
63
frontend/src/lib/components/TransportationCard.svelte
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
import Launch from '~icons/mdi/launch';
|
||||||
|
import TrashCanOutline from '~icons/mdi/trash-can-outline';
|
||||||
|
|
||||||
|
import FileDocumentEdit from '~icons/mdi/file-document-edit';
|
||||||
|
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import type { Collection, Transportation } from '$lib/types';
|
||||||
|
import { addToast } from '$lib/toasts';
|
||||||
|
|
||||||
|
import Plus from '~icons/mdi/plus';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
export let transportation: Transportation;
|
||||||
|
|
||||||
|
function editTransportation() {
|
||||||
|
dispatch('edit', transportation);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteTransportation() {
|
||||||
|
let res = await fetch(`/api/transportations/${transportation.id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
console.log('Error deleting transportation');
|
||||||
|
} else {
|
||||||
|
console.log('Collection deleted');
|
||||||
|
addToast('info', 'Transportation deleted successfully!');
|
||||||
|
dispatch('delete', transportation.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="card min-w-max lg:w-96 md:w-80 sm:w-60 xs:w-40 bg-primary-content shadow-xl overflow-hidden text-base-content"
|
||||||
|
>
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title overflow-ellipsis">{transportation.name}</h2>
|
||||||
|
<div class="badge badge-secondary">{transportation.type}</div>
|
||||||
|
{#if transportation.from_location && transportation.to_location}
|
||||||
|
<p class="text-sm">
|
||||||
|
{transportation.from_location} to {transportation.to_location}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
{#if transportation.date}
|
||||||
|
{new Date(transportation.date).toLocaleString()}
|
||||||
|
{/if}
|
||||||
|
<div class="card-actions justify-end">
|
||||||
|
<button on:click={deleteTransportation} class="btn btn-secondary"
|
||||||
|
><TrashCanOutline class="w-5 h-5 mr-1" /></button
|
||||||
|
>
|
||||||
|
<button class="btn btn-primary" on:click={editTransportation}>
|
||||||
|
<FileDocumentEdit class="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -64,6 +64,9 @@ export type Collection = {
|
||||||
is_public: boolean;
|
is_public: boolean;
|
||||||
adventures: Adventure[];
|
adventures: Adventure[];
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
|
start_date?: string;
|
||||||
|
end_date?: string;
|
||||||
|
transportations?: Transportation[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OpenStreetMapPlace = {
|
export type OpenStreetMapPlace = {
|
||||||
|
@ -82,3 +85,21 @@ export type OpenStreetMapPlace = {
|
||||||
display_name: string;
|
display_name: string;
|
||||||
boundingbox: string[];
|
boundingbox: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Transportation = {
|
||||||
|
id: number;
|
||||||
|
user_id: number;
|
||||||
|
type: string;
|
||||||
|
name: string;
|
||||||
|
description: string | null;
|
||||||
|
rating: number | null;
|
||||||
|
link: string | null;
|
||||||
|
date: string | null; // ISO 8601 date string
|
||||||
|
flight_number: string | null;
|
||||||
|
from_location: string | null;
|
||||||
|
to_location: string | null;
|
||||||
|
is_public: boolean;
|
||||||
|
collection: Collection | null;
|
||||||
|
created_at: string; // ISO 8601 date string
|
||||||
|
updated_at: string; // ISO 8601 date string
|
||||||
|
};
|
||||||
|
|
|
@ -64,6 +64,7 @@ export const actions: Actions = {
|
||||||
let link = formData.get('link') as string | null;
|
let link = formData.get('link') as string | null;
|
||||||
let latitude = formData.get('latitude') as string | null;
|
let latitude = formData.get('latitude') as string | null;
|
||||||
let longitude = formData.get('longitude') as string | null;
|
let longitude = formData.get('longitude') as string | null;
|
||||||
|
let collection = formData.get('collection') as string | null;
|
||||||
|
|
||||||
// check if latitude and longitude are valid
|
// check if latitude and longitude are valid
|
||||||
if (latitude && longitude) {
|
if (latitude && longitude) {
|
||||||
|
@ -108,6 +109,7 @@ export const actions: Actions = {
|
||||||
formDataToSend.append('description', description || '');
|
formDataToSend.append('description', description || '');
|
||||||
formDataToSend.append('latitude', latitude || '');
|
formDataToSend.append('latitude', latitude || '');
|
||||||
formDataToSend.append('longitude', longitude || '');
|
formDataToSend.append('longitude', longitude || '');
|
||||||
|
formDataToSend.append('collection', collection || '');
|
||||||
if (activity_types) {
|
if (activity_types) {
|
||||||
// Filter out empty and duplicate activity types, then trim each activity type
|
// Filter out empty and duplicate activity types, then trim each activity type
|
||||||
const cleanedActivityTypes = Array.from(
|
const cleanedActivityTypes = Array.from(
|
||||||
|
|
|
@ -15,7 +15,7 @@ export async function GET({ url, params, request, fetch, cookies }) {
|
||||||
|
|
||||||
/** @type {import('./$types').RequestHandler} */
|
/** @type {import('./$types').RequestHandler} */
|
||||||
export async function POST({ url, params, request, fetch, cookies }) {
|
export async function POST({ url, params, request, fetch, cookies }) {
|
||||||
return handleRequest(url, params, request, fetch, cookies);
|
return handleRequest(url, params, request, fetch, cookies, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function PATCH({ url, params, request, fetch, cookies }) {
|
export async function PATCH({ url, params, request, fetch, cookies }) {
|
||||||
|
@ -23,11 +23,11 @@ export async function PATCH({ url, params, request, fetch, cookies }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function PUT({ url, params, request, fetch, cookies }) {
|
export async function PUT({ url, params, request, fetch, cookies }) {
|
||||||
return handleRequest(url, params, request, fetch, cookies);
|
return handleRequest(url, params, request, fetch, cookies, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function DELETE({ url, params, request, fetch, cookies }) {
|
export async function DELETE({ url, params, request, fetch, cookies }) {
|
||||||
return handleRequest(url, params, request, fetch, cookies);
|
return handleRequest(url, params, request, fetch, cookies, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implement other HTTP methods as needed (PUT, DELETE, etc.)
|
// Implement other HTTP methods as needed (PUT, DELETE, etc.)
|
||||||
|
@ -62,6 +62,14 @@ async function handleRequest(
|
||||||
body: request.method !== 'GET' && request.method !== 'HEAD' ? await request.text() : undefined
|
body: request.method !== 'GET' && request.method !== 'HEAD' ? await request.text() : undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (response.status === 204) {
|
||||||
|
// For 204 No Content, return a response with no body
|
||||||
|
return new Response(null, {
|
||||||
|
status: 204,
|
||||||
|
headers: response.headers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const responseData = await response.text();
|
const responseData = await response.text();
|
||||||
|
|
||||||
return new Response(responseData, {
|
return new Response(responseData, {
|
||||||
|
|
|
@ -51,6 +51,8 @@ export const actions: Actions = {
|
||||||
|
|
||||||
const name = formData.get('name') as string;
|
const name = formData.get('name') as string;
|
||||||
const description = formData.get('description') as string | null;
|
const description = formData.get('description') as string | null;
|
||||||
|
const start_date = formData.get('start_date') as string | null;
|
||||||
|
const end_date = formData.get('end_date') as string | null;
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return {
|
return {
|
||||||
|
@ -62,6 +64,8 @@ export const actions: Actions = {
|
||||||
const formDataToSend = new FormData();
|
const formDataToSend = new FormData();
|
||||||
formDataToSend.append('name', name);
|
formDataToSend.append('name', name);
|
||||||
formDataToSend.append('description', description || '');
|
formDataToSend.append('description', description || '');
|
||||||
|
formDataToSend.append('start_date', start_date || '');
|
||||||
|
formDataToSend.append('end_date', end_date || '');
|
||||||
let auth = event.cookies.get('auth');
|
let auth = event.cookies.get('auth');
|
||||||
|
|
||||||
if (!auth) {
|
if (!auth) {
|
||||||
|
@ -136,6 +140,8 @@ export const actions: Actions = {
|
||||||
const name = formData.get('name') as string;
|
const name = formData.get('name') as string;
|
||||||
const description = formData.get('description') as string | null;
|
const description = formData.get('description') as string | null;
|
||||||
let is_public = formData.get('is_public') as string | null | boolean;
|
let is_public = formData.get('is_public') as string | null | boolean;
|
||||||
|
const start_date = formData.get('start_date') as string | null;
|
||||||
|
const end_date = formData.get('end_date') as string | null;
|
||||||
|
|
||||||
if (is_public) {
|
if (is_public) {
|
||||||
is_public = true;
|
is_public = true;
|
||||||
|
@ -154,6 +160,8 @@ export const actions: Actions = {
|
||||||
formDataToSend.append('name', name);
|
formDataToSend.append('name', name);
|
||||||
formDataToSend.append('description', description || '');
|
formDataToSend.append('description', description || '');
|
||||||
formDataToSend.append('is_public', is_public.toString());
|
formDataToSend.append('is_public', is_public.toString());
|
||||||
|
formDataToSend.append('start_date', start_date || '');
|
||||||
|
formDataToSend.append('end_date', end_date || '');
|
||||||
|
|
||||||
let auth = event.cookies.get('auth');
|
let auth = event.cookies.get('auth');
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Adventure, Collection } from '$lib/types';
|
import type { Adventure, Collection, Transportation } from '$lib/types';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
@ -10,6 +10,11 @@
|
||||||
import AdventureLink from '$lib/components/AdventureLink.svelte';
|
import AdventureLink from '$lib/components/AdventureLink.svelte';
|
||||||
import EditAdventure from '$lib/components/EditAdventure.svelte';
|
import EditAdventure from '$lib/components/EditAdventure.svelte';
|
||||||
import NotFound from '$lib/components/NotFound.svelte';
|
import NotFound from '$lib/components/NotFound.svelte';
|
||||||
|
import NewAdventure from '$lib/components/NewAdventure.svelte';
|
||||||
|
import { DefaultMarker, MapLibre, Popup } from 'svelte-maplibre';
|
||||||
|
import TransportationCard from '$lib/components/TransportationCard.svelte';
|
||||||
|
import EditTransportation from '$lib/components/EditTransportation.svelte';
|
||||||
|
import NewTransportation from '$lib/components/NewTransportation.svelte';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
|
||||||
|
@ -17,13 +22,18 @@
|
||||||
|
|
||||||
let adventures: Adventure[] = [];
|
let adventures: Adventure[] = [];
|
||||||
let numVisited: number = 0;
|
let numVisited: number = 0;
|
||||||
|
let transportations: Transportation[] = [];
|
||||||
|
|
||||||
|
let numberOfDays: number = NaN;
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
numVisited = adventures.filter((a) => a.type === 'visited').length;
|
numVisited = adventures.filter((a) => a.type === 'visited').length;
|
||||||
}
|
}
|
||||||
|
|
||||||
let notFound: boolean = false;
|
let notFound: boolean = false;
|
||||||
|
let isShowingLinkModal: boolean = false;
|
||||||
let isShowingCreateModal: boolean = false;
|
let isShowingCreateModal: boolean = false;
|
||||||
|
let isShowingTransportationModal: boolean = false;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (data.props.adventure) {
|
if (data.props.adventure) {
|
||||||
|
@ -32,12 +42,77 @@
|
||||||
} else {
|
} else {
|
||||||
notFound = true;
|
notFound = true;
|
||||||
}
|
}
|
||||||
|
if (collection.start_date && collection.end_date) {
|
||||||
|
numberOfDays =
|
||||||
|
Math.floor(
|
||||||
|
(new Date(collection.end_date).getTime() - new Date(collection.start_date).getTime()) /
|
||||||
|
(1000 * 60 * 60 * 24)
|
||||||
|
) + 1;
|
||||||
|
}
|
||||||
|
if (collection.transportations) {
|
||||||
|
transportations = collection.transportations;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function deleteAdventure(event: CustomEvent<number>) {
|
function deleteAdventure(event: CustomEvent<number>) {
|
||||||
adventures = adventures.filter((a) => a.id !== event.detail);
|
adventures = adventures.filter((a) => a.id !== event.detail);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function groupAdventuresByDate(
|
||||||
|
adventures: Adventure[],
|
||||||
|
startDate: Date
|
||||||
|
): Record<string, Adventure[]> {
|
||||||
|
const groupedAdventures: Record<string, Adventure[]> = {};
|
||||||
|
|
||||||
|
for (let i = 0; i < numberOfDays; i++) {
|
||||||
|
const currentDate = new Date(startDate);
|
||||||
|
currentDate.setDate(startDate.getDate() + i);
|
||||||
|
const dateString = currentDate.toISOString().split('T')[0];
|
||||||
|
groupedAdventures[dateString] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
adventures.forEach((adventure) => {
|
||||||
|
if (adventure.date) {
|
||||||
|
const adventureDate = new Date(adventure.date).toISOString().split('T')[0];
|
||||||
|
if (groupedAdventures[adventureDate]) {
|
||||||
|
groupedAdventures[adventureDate].push(adventure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return groupedAdventures;
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupTransportationsByDate(
|
||||||
|
transportations: Transportation[],
|
||||||
|
startDate: Date
|
||||||
|
): Record<string, Transportation[]> {
|
||||||
|
const groupedTransportations: Record<string, Transportation[]> = {};
|
||||||
|
|
||||||
|
for (let i = 0; i < numberOfDays; i++) {
|
||||||
|
const currentDate = new Date(startDate);
|
||||||
|
currentDate.setDate(startDate.getDate() + i);
|
||||||
|
const dateString = currentDate.toISOString().split('T')[0];
|
||||||
|
groupedTransportations[dateString] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
transportations.forEach((transportation) => {
|
||||||
|
if (transportation.date) {
|
||||||
|
const transportationDate = new Date(transportation.date).toISOString().split('T')[0];
|
||||||
|
if (groupedTransportations[transportationDate]) {
|
||||||
|
groupedTransportations[transportationDate].push(transportation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return groupedTransportations;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAdventure(event: CustomEvent<Adventure>) {
|
||||||
|
adventures = [event.detail, ...adventures];
|
||||||
|
isShowingCreateModal = false;
|
||||||
|
}
|
||||||
|
|
||||||
async function addAdventure(event: CustomEvent<Adventure>) {
|
async function addAdventure(event: CustomEvent<Adventure>) {
|
||||||
console.log(event.detail);
|
console.log(event.detail);
|
||||||
if (adventures.find((a) => a.id === event.detail.id)) {
|
if (adventures.find((a) => a.id === event.detail.id)) {
|
||||||
|
@ -75,13 +150,27 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
let adventureToEdit: Adventure;
|
let adventureToEdit: Adventure;
|
||||||
|
let transportationToEdit: Transportation;
|
||||||
let isEditModalOpen: boolean = false;
|
let isEditModalOpen: boolean = false;
|
||||||
|
let isTransportationEditModalOpen: boolean = false;
|
||||||
|
|
||||||
|
let newType: string;
|
||||||
|
|
||||||
function editAdventure(event: CustomEvent<Adventure>) {
|
function editAdventure(event: CustomEvent<Adventure>) {
|
||||||
adventureToEdit = event.detail;
|
adventureToEdit = event.detail;
|
||||||
isEditModalOpen = true;
|
isEditModalOpen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function saveNewTransportation(event: CustomEvent<Transportation>) {
|
||||||
|
transportations = transportations.map((transportation) => {
|
||||||
|
if (transportation.id === event.detail.id) {
|
||||||
|
return event.detail;
|
||||||
|
}
|
||||||
|
return transportation;
|
||||||
|
});
|
||||||
|
isTransportationEditModalOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
function saveEdit(event: CustomEvent<Adventure>) {
|
function saveEdit(event: CustomEvent<Adventure>) {
|
||||||
adventures = adventures.map((adventure) => {
|
adventures = adventures.map((adventure) => {
|
||||||
if (adventure.id === event.detail.id) {
|
if (adventure.id === event.detail.id) {
|
||||||
|
@ -93,16 +182,24 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isShowingCreateModal}
|
{#if isShowingLinkModal}
|
||||||
<AdventureLink
|
<AdventureLink
|
||||||
user={data?.user ?? null}
|
user={data?.user ?? null}
|
||||||
on:close={() => {
|
on:close={() => {
|
||||||
isShowingCreateModal = false;
|
isShowingLinkModal = false;
|
||||||
}}
|
}}
|
||||||
on:add={addAdventure}
|
on:add={addAdventure}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if isTransportationEditModalOpen}
|
||||||
|
<EditTransportation
|
||||||
|
{transportationToEdit}
|
||||||
|
on:close={() => (isTransportationEditModalOpen = false)}
|
||||||
|
on:saveEdit={saveNewTransportation}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if isEditModalOpen}
|
{#if isEditModalOpen}
|
||||||
<EditAdventure
|
<EditAdventure
|
||||||
{adventureToEdit}
|
{adventureToEdit}
|
||||||
|
@ -111,6 +208,26 @@
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if isShowingCreateModal}
|
||||||
|
<NewAdventure
|
||||||
|
type={newType}
|
||||||
|
collection_id={collection.id}
|
||||||
|
on:create={createAdventure}
|
||||||
|
on:close={() => (isShowingCreateModal = false)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if isShowingTransportationModal}
|
||||||
|
<NewTransportation
|
||||||
|
on:close={() => (isShowingTransportationModal = false)}
|
||||||
|
on:add={(event) => {
|
||||||
|
transportations = [event.detail, ...transportations];
|
||||||
|
isShowingTransportationModal = false;
|
||||||
|
}}
|
||||||
|
{collection}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if notFound}
|
{#if notFound}
|
||||||
<div
|
<div
|
||||||
class="flex min-h-[100dvh] flex-col items-center justify-center bg-background px-4 py-12 sm:px-6 lg:px-8 -mt-20"
|
class="flex min-h-[100dvh] flex-col items-center justify-center bg-background px-4 py-12 sm:px-6 lg:px-8 -mt-20"
|
||||||
|
@ -139,35 +256,83 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if collection}
|
{#if collection}
|
||||||
<div class="fixed bottom-4 right-4 z-[999]">
|
{#if data.user}
|
||||||
<div class="flex flex-row items-center justify-center gap-4">
|
<div class="fixed bottom-4 right-4 z-[999]">
|
||||||
<div class="dropdown dropdown-top dropdown-end">
|
<div class="flex flex-row items-center justify-center gap-4">
|
||||||
<div tabindex="0" role="button" class="btn m-1 size-16 btn-primary">
|
<div class="dropdown dropdown-top dropdown-end">
|
||||||
<Plus class="w-8 h-8" />
|
<div tabindex="0" role="button" class="btn m-1 size-16 btn-primary">
|
||||||
</div>
|
<Plus class="w-8 h-8" />
|
||||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
</div>
|
||||||
<ul
|
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||||
tabindex="0"
|
<ul
|
||||||
class="dropdown-content z-[1] menu p-4 shadow bg-base-300 text-base-content rounded-box w-52 gap-4"
|
tabindex="0"
|
||||||
>
|
class="dropdown-content z-[1] menu p-4 shadow bg-base-300 text-base-content rounded-box w-52 gap-4"
|
||||||
<p class="text-center font-bold text-lg">Link new...</p>
|
|
||||||
<button
|
|
||||||
class="btn btn-primary"
|
|
||||||
on:click={() => {
|
|
||||||
isShowingCreateModal = true;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Adventure</button
|
|
||||||
>
|
>
|
||||||
|
<p class="text-center font-bold text-lg">Link new...</p>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
on:click={() => {
|
||||||
|
isShowingLinkModal = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Adventure</button
|
||||||
|
>
|
||||||
|
<p class="text-center font-bold text-lg">Add new...</p>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
on:click={() => {
|
||||||
|
isShowingCreateModal = true;
|
||||||
|
newType = 'visited';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Visited Adventure</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
on:click={() => {
|
||||||
|
isShowingCreateModal = true;
|
||||||
|
newType = 'planned';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Planned Adventure</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
on:click={() => {
|
||||||
|
isShowingCreateModal = true;
|
||||||
|
newType = 'lodging';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Lodging</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
on:click={() => {
|
||||||
|
isShowingCreateModal = true;
|
||||||
|
newType = 'dining';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Dining</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
on:click={() => {
|
||||||
|
isShowingTransportationModal = true;
|
||||||
|
newType = '';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Transportation</button
|
||||||
|
>
|
||||||
|
|
||||||
<!-- <button
|
<!-- <button
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
on:click={() => (isShowingNewTrip = true)}>Trip Planner</button
|
on:click={() => (isShowingNewTrip = true)}>Trip Planner</button
|
||||||
> -->
|
> -->
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
{#if collection.name}
|
{#if collection.name}
|
||||||
<h1 class="text-center font-extrabold text-4xl mb-2">{collection.name}</h1>
|
<h1 class="text-center font-extrabold text-4xl mb-2">{collection.name}</h1>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -186,7 +351,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<h1 class="text-center font-semibold text-2xl mt-4 mb-2">Linked Adventures</h1>
|
<h1 class="text-center font-bold text-4xl mt-4 mb-2">Linked Adventures</h1>
|
||||||
{#if adventures.length == 0}
|
{#if adventures.length == 0}
|
||||||
<NotFound error={undefined} />
|
<NotFound error={undefined} />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -203,7 +368,113 @@
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if collection.description}
|
{#if collection.transportations && collection.transportations.length > 0}
|
||||||
<p class="text-center text-lg mt-4 pl-16 pr-16">{collection.description}</p>
|
<h1 class="text-center font-bold text-4xl mt-4 mb-4">Transportation</h1>
|
||||||
|
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
|
||||||
|
{#each transportations as transportation}
|
||||||
|
<TransportationCard
|
||||||
|
{transportation}
|
||||||
|
on:delete={(event) => {
|
||||||
|
transportations = transportations.filter((t) => t.id != event.detail);
|
||||||
|
}}
|
||||||
|
on:edit={(event) => {
|
||||||
|
transportationToEdit = event.detail;
|
||||||
|
isTransportationEditModalOpen = true;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if collection.start_date && collection.end_date}
|
||||||
|
<h1 class="text-center font-bold text-4xl mt-4">Itinerary by Date</h1>
|
||||||
|
{#if numberOfDays}
|
||||||
|
<p class="text-center text-lg pl-16 pr-16">Duration: {numberOfDays} days</p>
|
||||||
|
{/if}
|
||||||
|
<p class="text-center text-lg pl-16 pr-16">
|
||||||
|
Dates: {new Date(collection.start_date).toLocaleDateString('en-US', { timeZone: 'UTC' })} - {new Date(
|
||||||
|
collection.end_date
|
||||||
|
).toLocaleDateString('en-US', { timeZone: 'UTC' })}
|
||||||
|
</p>
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
{#each Array(numberOfDays) as _, i}
|
||||||
|
{@const currentDate = new Date(collection.start_date)}
|
||||||
|
{@const temp = currentDate.setDate(currentDate.getDate() + i)}
|
||||||
|
{@const dateString = currentDate.toISOString().split('T')[0]}
|
||||||
|
{@const dayAdventures = groupAdventuresByDate(adventures, new Date(collection.start_date))[
|
||||||
|
dateString
|
||||||
|
]}
|
||||||
|
{@const dayTransportations = groupTransportationsByDate(
|
||||||
|
transportations,
|
||||||
|
new Date(collection.start_date)
|
||||||
|
)[dateString]}
|
||||||
|
|
||||||
|
<h2 class="text-center font-semibold text-2xl mb-2 mt-4">
|
||||||
|
Day {i + 1} - {currentDate.toLocaleDateString('en-US', { timeZone: 'UTC' })}
|
||||||
|
</h2>
|
||||||
|
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
|
||||||
|
{#if dayAdventures.length > 0}
|
||||||
|
{#each dayAdventures as adventure}
|
||||||
|
<AdventureCard
|
||||||
|
user={data.user}
|
||||||
|
on:edit={editAdventure}
|
||||||
|
on:delete={deleteAdventure}
|
||||||
|
type={adventure.type}
|
||||||
|
{adventure}
|
||||||
|
on:typeChange={changeType}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
{#if dayTransportations.length > 0}
|
||||||
|
{#each dayTransportations as transportation}
|
||||||
|
<TransportationCard
|
||||||
|
{transportation}
|
||||||
|
on:delete={(event) => {
|
||||||
|
transportations = transportations.filter((t) => t.id != event.detail);
|
||||||
|
}}
|
||||||
|
on:edit={(event) => {
|
||||||
|
transportationToEdit = event.detail;
|
||||||
|
isTransportationEditModalOpen = true;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
{#if dayAdventures.length == 0 && dayTransportations.length == 0}
|
||||||
|
<p class="text-center text-lg mt-2">
|
||||||
|
No adventures or transportaions planned for this day.
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<MapLibre
|
||||||
|
style="https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"
|
||||||
|
class="flex items-center self-center justify-center aspect-[9/16] max-h-[70vh] sm:aspect-video sm:max-h-full w-10/12"
|
||||||
|
standardControls
|
||||||
|
>
|
||||||
|
<!-- MapEvents gives you access to map events even from other components inside the map,
|
||||||
|
where you might not have access to the top-level `MapLibre` component. In this case
|
||||||
|
it would also work to just use on:click on the MapLibre component itself. -->
|
||||||
|
<!-- <MapEvents on:click={addMarker} /> -->
|
||||||
|
|
||||||
|
{#each adventures as adventure}
|
||||||
|
{#if adventure.longitude && adventure.latitude}
|
||||||
|
<DefaultMarker lngLat={{ lng: adventure.longitude, lat: adventure.latitude }}>
|
||||||
|
<Popup openOn="click" offset={[0, -10]}>
|
||||||
|
<div class="text-lg text-black font-bold">{adventure.name}</div>
|
||||||
|
<p class="font-semibold text-black text-md">
|
||||||
|
{adventure.type.charAt(0).toUpperCase() + adventure.type.slice(1)}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{adventure.date
|
||||||
|
? new Date(adventure.date).toLocaleDateString('en-US', { timeZone: 'UTC' })
|
||||||
|
: ''}
|
||||||
|
</p>
|
||||||
|
</Popup>
|
||||||
|
</DefaultMarker>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</MapLibre>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -5,12 +5,6 @@ import type { Adventure, VisitedRegion } from '$lib/types';
|
||||||
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||||
|
|
||||||
export const load = (async (event) => {
|
export const load = (async (event) => {
|
||||||
let countryCodesToFetch = ['FR', 'US', 'CA', 'DE', 'AU', 'MX', 'JP'];
|
|
||||||
let geoJSON = {
|
|
||||||
type: 'FeatureCollection',
|
|
||||||
features: []
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!event.locals.user) {
|
if (!event.locals.user) {
|
||||||
return redirect(302, '/login');
|
return redirect(302, '/login');
|
||||||
} else {
|
} else {
|
||||||
|
@ -27,19 +21,6 @@ export const load = (async (event) => {
|
||||||
});
|
});
|
||||||
let visitedRegions = (await visitedRegionsFetch.json()) as VisitedRegion[];
|
let visitedRegions = (await visitedRegionsFetch.json()) as VisitedRegion[];
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
countryCodesToFetch.map(async (code) => {
|
|
||||||
let res = await fetch(`${endpoint}/static/data/${code.toLowerCase()}.json`);
|
|
||||||
console.log('fetching ' + code);
|
|
||||||
let json = await res.json();
|
|
||||||
if (!json) {
|
|
||||||
console.error(`Failed to fetch ${code} GeoJSON`);
|
|
||||||
} else {
|
|
||||||
geoJSON.features = geoJSON.features.concat(json.features);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!visitedFetch.ok) {
|
if (!visitedFetch.ok) {
|
||||||
console.error('Failed to fetch visited adventures');
|
console.error('Failed to fetch visited adventures');
|
||||||
return redirect(302, '/login');
|
return redirect(302, '/login');
|
||||||
|
@ -61,7 +42,6 @@ export const load = (async (event) => {
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
markers,
|
markers,
|
||||||
geoJSON,
|
|
||||||
visitedRegions
|
visitedRegions
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,6 +17,24 @@
|
||||||
|
|
||||||
let clickedName = '';
|
let clickedName = '';
|
||||||
|
|
||||||
|
let showVisited = true;
|
||||||
|
let showPlanned = true;
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (!showVisited) {
|
||||||
|
markers = markers.filter((marker) => marker.type !== 'visited');
|
||||||
|
} else {
|
||||||
|
const visitedMarkers = data.props.markers.filter((marker) => marker.type === 'visited');
|
||||||
|
markers = [...markers, ...visitedMarkers];
|
||||||
|
}
|
||||||
|
if (!showPlanned) {
|
||||||
|
markers = markers.filter((marker) => marker.type !== 'planned');
|
||||||
|
} else {
|
||||||
|
const plannedMarkers = data.props.markers.filter((marker) => marker.type === 'planned');
|
||||||
|
markers = [...markers, ...plannedMarkers];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let newMarker = [];
|
let newMarker = [];
|
||||||
|
|
||||||
let newLongitude = null;
|
let newLongitude = null;
|
||||||
|
@ -61,7 +79,7 @@
|
||||||
|
|
||||||
let visitedRegions = data.props.visitedRegions;
|
let visitedRegions = data.props.visitedRegions;
|
||||||
|
|
||||||
let geoJSON = data.props.geoJSON;
|
let geoJSON = [];
|
||||||
|
|
||||||
let visitArray = [];
|
let visitArray = [];
|
||||||
|
|
||||||
|
@ -77,11 +95,29 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// mapped to the checkbox
|
// mapped to the checkbox
|
||||||
let showGEO = true;
|
let showGEO = false;
|
||||||
|
$: {
|
||||||
|
if (showGEO && geoJSON.length === 0) {
|
||||||
|
(async () => {
|
||||||
|
geoJSON = await fetch('/api/geojson/').then((res) => res.json());
|
||||||
|
})();
|
||||||
|
} else if (!showGEO) {
|
||||||
|
geoJSON = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let createModalOpen = false;
|
let createModalOpen = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<label class="label cursor-pointer">
|
||||||
|
<span class="label-text">Visited</span>
|
||||||
|
<input type="checkbox" bind:checked={showVisited} class="checkbox checkbox-primary" />
|
||||||
|
</label>
|
||||||
|
<label class="label cursor-pointer">
|
||||||
|
<span class="label-text">Planned</span>
|
||||||
|
<input type="checkbox" bind:checked={showPlanned} class="checkbox checkbox-primary" />
|
||||||
|
</label>
|
||||||
|
|
||||||
{#if newMarker.length > 0}
|
{#if newMarker.length > 0}
|
||||||
<button type="button" class="btn btn-primary mb-2" on:click={() => (createModalOpen = true)}
|
<button type="button" class="btn btn-primary mb-2" on:click={() => (createModalOpen = true)}
|
||||||
>Add New Adventure at Marker</button
|
>Add New Adventure at Marker</button
|
||||||
|
@ -156,7 +192,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
{#if showGEO}
|
{#if showGEO}
|
||||||
<GeoJSON id="states" data={data.props.geoJSON} promoteId="ISOCODE">
|
<GeoJSON id="states" data={geoJSON} promoteId="ISOCODE">
|
||||||
<LineLayer
|
<LineLayer
|
||||||
layout={{ 'line-cap': 'round', 'line-join': 'round' }}
|
layout={{ 'line-cap': 'round', 'line-join': 'round' }}
|
||||||
paint={{ 'line-color': 'grey', 'line-width': 3 }}
|
paint={{ 'line-color': 'grey', 'line-width': 3 }}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue