mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-07-24 15:29:36 +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
|
||||
from django.contrib import admin
|
||||
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
|
||||
|
||||
|
||||
|
@ -74,6 +74,7 @@ admin.site.register(Collection, CollectionAdmin)
|
|||
admin.site.register(Country, CountryAdmin)
|
||||
admin.site.register(Region, RegionAdmin)
|
||||
admin.site.register(VisitedRegion)
|
||||
admin.site.register(Transportation)
|
||||
|
||||
admin.site.site_header = 'AdventureLog Admin'
|
||||
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 = [
|
||||
('visited', 'Visited'),
|
||||
('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)
|
||||
is_public = models.BooleanField(default=False)
|
||||
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
|
||||
def clean(self):
|
||||
|
@ -66,3 +81,33 @@ class Collection(models.Model):
|
|||
|
||||
def __str__(self):
|
||||
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
|
||||
from .models import Adventure, Collection
|
||||
from .models import Adventure, Collection, Transportation
|
||||
from rest_framework import serializers
|
||||
|
||||
class AdventureSerializer(serializers.ModelSerializer):
|
||||
|
@ -23,13 +23,49 @@ class AdventureSerializer(serializers.ModelSerializer):
|
|||
return [activity.lower() for activity in 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):
|
||||
adventures = AdventureSerializer(many=True, read_only=True, source='adventure_set')
|
||||
transportations = TransportationSerializer(many=True, read_only=True, source='transportation_set')
|
||||
|
||||
class Meta:
|
||||
model = Collection
|
||||
# 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 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.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'generate', GenerateDescription, basename='generate')
|
||||
router.register(r'activity-types', ActivityTypesView, basename='activity-types')
|
||||
router.register(r'transportations', TransportationViewSet, basename='transportations')
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
|
|
|
@ -4,9 +4,9 @@ from rest_framework.decorators import action
|
|||
from rest_framework import viewsets
|
||||
from django.db.models.functions import Lower
|
||||
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 .serializers import AdventureSerializer, CollectionSerializer
|
||||
from .serializers import AdventureSerializer, CollectionSerializer, TransportationSerializer
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from django.db.models import Q, Prefetch
|
||||
from .permissions import IsOwnerOrReadOnly, IsPublicReadOnly
|
||||
|
@ -138,8 +138,9 @@ class AdventureViewSet(viewsets.ModelViewSet):
|
|||
# queryset = Adventure.objects.filter(
|
||||
# Q(is_public=True) | Q(user_id=request.user.id), collection=None
|
||||
# )
|
||||
allowed_types = ['visited', 'planned']
|
||||
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)
|
||||
|
@ -237,6 +238,9 @@ class CollectionViewSet(viewsets.ModelViewSet):
|
|||
# Update associated adventures to match the collection's is_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)
|
||||
action = "public" if new_public_status else "private"
|
||||
print(f"Collection {instance.id} and its adventures were set to {action}")
|
||||
|
@ -257,31 +261,15 @@ class CollectionViewSet(viewsets.ModelViewSet):
|
|||
Prefetch('adventure_set', queryset=Adventure.objects.filter(
|
||||
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)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
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):
|
||||
paginator = self.pagination_class()
|
||||
|
@ -378,3 +366,40 @@ class ActivityTypesView(viewsets.ViewSet):
|
|||
allTypes.append(x)
|
||||
|
||||
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 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.register(r'countries', CountryViewSet, basename='countries')
|
||||
|
@ -13,4 +13,5 @@ urlpatterns = [
|
|||
path('', include(router.urls)),
|
||||
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('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 rest_framework.response import Response
|
||||
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'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
|
@ -49,4 +53,38 @@ class VisitedRegionViewSet(viewsets.ModelViewSet):
|
|||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_create(serializer)
|
||||
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 keyword: string = '';
|
||||
|
||||
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[] = [];
|
||||
// makes it reactivty to changes so it updates automatically
|
||||
$: {
|
||||
|
@ -149,9 +161,16 @@
|
|||
<div>
|
||||
{#if adventure.type == 'visited' && user?.pk == adventure.user_id}
|
||||
<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>
|
||||
{: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}
|
||||
|
||||
<div class="badge badge-neutral">{adventure.is_public ? 'Public' : 'Private'}</div>
|
||||
</div>
|
||||
{#if adventure.location && adventure.location !== ''}
|
||||
|
@ -163,7 +182,7 @@
|
|||
{#if adventure.date && adventure.date !== ''}
|
||||
<div class="inline-flex items-center">
|
||||
<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>
|
||||
{/if}
|
||||
{#if adventure.activity_types && adventure.activity_types.length > 0}
|
||||
|
@ -194,7 +213,7 @@
|
|||
><Launch class="w-6 h-6" />Open Details</button
|
||||
>
|
||||
<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>
|
||||
{#if adventure.type == 'visited'}
|
||||
<button class="btn btn-neutral mb-2" on:click={changeType('planned')}
|
||||
|
@ -206,11 +225,18 @@
|
|||
><CheckBold class="w-6 h-6" />Mark Visited</button
|
||||
>
|
||||
{/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}
|
||||
><LinkVariantRemove class="w-6 h-6" />Remove from Collection</button
|
||||
>
|
||||
{/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}
|
||||
<button class="btn btn-neutral mb-2" on:click={() => (isCollectionModalOpen = true)}
|
||||
><Plus class="w-6 h-6" />Add to Collection</button
|
||||
|
|
|
@ -47,6 +47,20 @@
|
|||
<div class="card-body">
|
||||
<h2 class="card-title overflow-ellipsis">{collection.name}</h2>
|
||||
<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">
|
||||
{#if type != 'link'}
|
||||
<button on:click={deleteCollection} class="btn btn-secondary"
|
||||
|
|
|
@ -222,20 +222,22 @@
|
|||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="activityTypes"
|
||||
>Activity Types <ClipboardList class="inline-block -mt-1 mb-1 w-6 h-6" /></label
|
||||
><br />
|
||||
<input
|
||||
type="text"
|
||||
id="activity_types"
|
||||
name="activity_types"
|
||||
hidden
|
||||
bind:value={adventureToEdit.activity_types}
|
||||
class="input input-bordered w-full max-w-xs mt-1"
|
||||
/>
|
||||
<ActivityComplete bind:activities={adventureToEdit.activity_types} />
|
||||
</div>
|
||||
{#if adventureToEdit.type == 'visited' || adventureToEdit.type == 'planned'}
|
||||
<div class="mb-2">
|
||||
<label for="activityTypes"
|
||||
>Activity Types <ClipboardList class="inline-block -mt-1 mb-1 w-6 h-6" /></label
|
||||
><br />
|
||||
<input
|
||||
type="text"
|
||||
id="activity_types"
|
||||
name="activity_types"
|
||||
hidden
|
||||
bind:value={adventureToEdit.activity_types}
|
||||
class="input input-bordered w-full max-w-xs mt-1"
|
||||
/>
|
||||
<ActivityComplete bind:activities={adventureToEdit.activity_types} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="mb-2">
|
||||
<label for="image">Image </label><br />
|
||||
<div class="flex">
|
||||
|
|
|
@ -109,6 +109,7 @@
|
|||
bind:value={collectionToEdit.description}
|
||||
class="input input-bordered w-full max-w-xs mt-1 mb-2"
|
||||
/>
|
||||
|
||||
<!-- <button
|
||||
class="btn btn-neutral ml-2"
|
||||
type="button"
|
||||
|
@ -117,6 +118,28 @@
|
|||
></iconify-icon>Generate Description</button
|
||||
> -->
|
||||
</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 class="mb-2">
|
||||
<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 latitude: number | null = null;
|
||||
export let collection_id: number | null = null;
|
||||
|
||||
import MapMarker from '~icons/mdi/map-marker';
|
||||
import Calendar from '~icons/mdi/calendar';
|
||||
|
@ -23,6 +24,7 @@
|
|||
import Wikipedia from '~icons/mdi/wikipedia';
|
||||
import ActivityComplete from './ActivityComplete.svelte';
|
||||
import { appVersion } from '$lib/config';
|
||||
import AdventureCard from './AdventureCard.svelte';
|
||||
|
||||
let newAdventure: Adventure = {
|
||||
id: NaN,
|
||||
|
@ -39,7 +41,7 @@
|
|||
latitude: null,
|
||||
longitude: null,
|
||||
is_public: false,
|
||||
collection: null
|
||||
collection: collection_id || NaN
|
||||
};
|
||||
|
||||
if (longitude && latitude) {
|
||||
|
@ -293,20 +295,22 @@
|
|||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="activityTypes"
|
||||
>Activity Types <ClipboardList class="inline-block -mt-1 mb-1 w-6 h-6" /></label
|
||||
><br />
|
||||
<input
|
||||
type="text"
|
||||
id="activity_types"
|
||||
name="activity_types"
|
||||
hidden
|
||||
bind:value={newAdventure.activity_types}
|
||||
class="input input-bordered w-full max-w-xs mt-1"
|
||||
/>
|
||||
<ActivityComplete bind:activities={newAdventure.activity_types} />
|
||||
</div>
|
||||
{#if newAdventure.type == 'visited' || newAdventure.type == 'planned'}
|
||||
<div class="mb-2">
|
||||
<label for="activityTypes"
|
||||
>Activity Types <ClipboardList class="inline-block -mt-1 mb-1 w-6 h-6" /></label
|
||||
><br />
|
||||
<input
|
||||
type="text"
|
||||
id="activity_types"
|
||||
name="activity_types"
|
||||
hidden
|
||||
bind:value={newAdventure.activity_types}
|
||||
class="input input-bordered w-full max-w-xs mt-1"
|
||||
/>
|
||||
<ActivityComplete bind:activities={newAdventure.activity_types} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="mb-2">
|
||||
<label for="rating"
|
||||
>Rating <iconify-icon icon="mdi:star" class="text-xl -mb-1"></iconify-icon></label
|
||||
|
@ -371,6 +375,14 @@
|
|||
bind:value={newAdventure.longitude}
|
||||
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="button" class="btn mt-4" on:click={close}>Close</button>
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
import { enhance } from '$app/forms';
|
||||
import { addToast } from '$lib/toasts';
|
||||
|
||||
import Calendar from '~icons/mdi/calendar';
|
||||
|
||||
let newCollection: Collection = {
|
||||
user_id: NaN,
|
||||
id: NaN,
|
||||
|
@ -104,10 +106,32 @@
|
|||
class="input input-bordered w-full max-w-xs mt-1 mb-2"
|
||||
/>
|
||||
</div>
|
||||
</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 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={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>
|
||||
</form>
|
||||
</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) {
|
||||
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);
|
||||
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;
|
||||
adventures: Adventure[];
|
||||
created_at?: string;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
transportations?: Transportation[];
|
||||
};
|
||||
|
||||
export type OpenStreetMapPlace = {
|
||||
|
@ -82,3 +85,21 @@ export type OpenStreetMapPlace = {
|
|||
display_name: 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 latitude = formData.get('latitude') 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
|
||||
if (latitude && longitude) {
|
||||
|
@ -108,6 +109,7 @@ export const actions: Actions = {
|
|||
formDataToSend.append('description', description || '');
|
||||
formDataToSend.append('latitude', latitude || '');
|
||||
formDataToSend.append('longitude', longitude || '');
|
||||
formDataToSend.append('collection', collection || '');
|
||||
if (activity_types) {
|
||||
// Filter out empty and duplicate activity types, then trim each activity type
|
||||
const cleanedActivityTypes = Array.from(
|
||||
|
|
|
@ -15,7 +15,7 @@ export async function GET({ url, params, request, fetch, cookies }) {
|
|||
|
||||
/** @type {import('./$types').RequestHandler} */
|
||||
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 }) {
|
||||
|
@ -23,11 +23,11 @@ export async function PATCH({ 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 }) {
|
||||
return handleRequest(url, params, request, fetch, cookies);
|
||||
return handleRequest(url, params, request, fetch, cookies, true);
|
||||
}
|
||||
|
||||
// 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
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
return new Response(responseData, {
|
||||
|
|
|
@ -51,6 +51,8 @@ export const actions: Actions = {
|
|||
|
||||
const name = formData.get('name') as string;
|
||||
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) {
|
||||
return {
|
||||
|
@ -62,6 +64,8 @@ export const actions: Actions = {
|
|||
const formDataToSend = new FormData();
|
||||
formDataToSend.append('name', name);
|
||||
formDataToSend.append('description', description || '');
|
||||
formDataToSend.append('start_date', start_date || '');
|
||||
formDataToSend.append('end_date', end_date || '');
|
||||
let auth = event.cookies.get('auth');
|
||||
|
||||
if (!auth) {
|
||||
|
@ -136,6 +140,8 @@ export const actions: Actions = {
|
|||
const name = formData.get('name') as string;
|
||||
const description = formData.get('description') as string | null;
|
||||
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) {
|
||||
is_public = true;
|
||||
|
@ -154,6 +160,8 @@ export const actions: Actions = {
|
|||
formDataToSend.append('name', name);
|
||||
formDataToSend.append('description', description || '');
|
||||
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');
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import type { Adventure, Collection } from '$lib/types';
|
||||
import type { Adventure, Collection, Transportation } from '$lib/types';
|
||||
import { onMount } from 'svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { goto } from '$app/navigation';
|
||||
|
@ -10,6 +10,11 @@
|
|||
import AdventureLink from '$lib/components/AdventureLink.svelte';
|
||||
import EditAdventure from '$lib/components/EditAdventure.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;
|
||||
|
||||
|
@ -17,13 +22,18 @@
|
|||
|
||||
let adventures: Adventure[] = [];
|
||||
let numVisited: number = 0;
|
||||
let transportations: Transportation[] = [];
|
||||
|
||||
let numberOfDays: number = NaN;
|
||||
|
||||
$: {
|
||||
numVisited = adventures.filter((a) => a.type === 'visited').length;
|
||||
}
|
||||
|
||||
let notFound: boolean = false;
|
||||
let isShowingLinkModal: boolean = false;
|
||||
let isShowingCreateModal: boolean = false;
|
||||
let isShowingTransportationModal: boolean = false;
|
||||
|
||||
onMount(() => {
|
||||
if (data.props.adventure) {
|
||||
|
@ -32,12 +42,77 @@
|
|||
} else {
|
||||
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>) {
|
||||
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>) {
|
||||
console.log(event.detail);
|
||||
if (adventures.find((a) => a.id === event.detail.id)) {
|
||||
|
@ -75,13 +150,27 @@
|
|||
}
|
||||
|
||||
let adventureToEdit: Adventure;
|
||||
let transportationToEdit: Transportation;
|
||||
let isEditModalOpen: boolean = false;
|
||||
let isTransportationEditModalOpen: boolean = false;
|
||||
|
||||
let newType: string;
|
||||
|
||||
function editAdventure(event: CustomEvent<Adventure>) {
|
||||
adventureToEdit = event.detail;
|
||||
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>) {
|
||||
adventures = adventures.map((adventure) => {
|
||||
if (adventure.id === event.detail.id) {
|
||||
|
@ -93,16 +182,24 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
{#if isShowingCreateModal}
|
||||
{#if isShowingLinkModal}
|
||||
<AdventureLink
|
||||
user={data?.user ?? null}
|
||||
on:close={() => {
|
||||
isShowingCreateModal = false;
|
||||
isShowingLinkModal = false;
|
||||
}}
|
||||
on:add={addAdventure}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if isTransportationEditModalOpen}
|
||||
<EditTransportation
|
||||
{transportationToEdit}
|
||||
on:close={() => (isTransportationEditModalOpen = false)}
|
||||
on:saveEdit={saveNewTransportation}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if isEditModalOpen}
|
||||
<EditAdventure
|
||||
{adventureToEdit}
|
||||
|
@ -111,6 +208,26 @@
|
|||
/>
|
||||
{/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}
|
||||
<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"
|
||||
|
@ -139,35 +256,83 @@
|
|||
</div>
|
||||
{/if}
|
||||
{#if collection}
|
||||
<div class="fixed bottom-4 right-4 z-[999]">
|
||||
<div class="flex flex-row items-center justify-center gap-4">
|
||||
<div class="dropdown dropdown-top dropdown-end">
|
||||
<div tabindex="0" role="button" class="btn m-1 size-16 btn-primary">
|
||||
<Plus class="w-8 h-8" />
|
||||
</div>
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<ul
|
||||
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
|
||||
{#if data.user}
|
||||
<div class="fixed bottom-4 right-4 z-[999]">
|
||||
<div class="flex flex-row items-center justify-center gap-4">
|
||||
<div class="dropdown dropdown-top dropdown-end">
|
||||
<div tabindex="0" role="button" class="btn m-1 size-16 btn-primary">
|
||||
<Plus class="w-8 h-8" />
|
||||
</div>
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<ul
|
||||
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={() => {
|
||||
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"
|
||||
on:click={() => (isShowingNewTrip = true)}>Trip Planner</button
|
||||
> -->
|
||||
</ul>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if collection.name}
|
||||
<h1 class="text-center font-extrabold text-4xl mb-2">{collection.name}</h1>
|
||||
{/if}
|
||||
|
@ -186,7 +351,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{/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}
|
||||
<NotFound error={undefined} />
|
||||
{/if}
|
||||
|
@ -203,7 +368,113 @@
|
|||
{/each}
|
||||
</div>
|
||||
|
||||
{#if collection.description}
|
||||
<p class="text-center text-lg mt-4 pl-16 pr-16">{collection.description}</p>
|
||||
{#if collection.transportations && collection.transportations.length > 0}
|
||||
<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}
|
||||
|
|
|
@ -5,12 +5,6 @@ import type { Adventure, VisitedRegion } from '$lib/types';
|
|||
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||
|
||||
export const load = (async (event) => {
|
||||
let countryCodesToFetch = ['FR', 'US', 'CA', 'DE', 'AU', 'MX', 'JP'];
|
||||
let geoJSON = {
|
||||
type: 'FeatureCollection',
|
||||
features: []
|
||||
};
|
||||
|
||||
if (!event.locals.user) {
|
||||
return redirect(302, '/login');
|
||||
} else {
|
||||
|
@ -27,19 +21,6 @@ export const load = (async (event) => {
|
|||
});
|
||||
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) {
|
||||
console.error('Failed to fetch visited adventures');
|
||||
return redirect(302, '/login');
|
||||
|
@ -61,7 +42,6 @@ export const load = (async (event) => {
|
|||
return {
|
||||
props: {
|
||||
markers,
|
||||
geoJSON,
|
||||
visitedRegions
|
||||
}
|
||||
};
|
||||
|
|
|
@ -17,6 +17,24 @@
|
|||
|
||||
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 newLongitude = null;
|
||||
|
@ -61,7 +79,7 @@
|
|||
|
||||
let visitedRegions = data.props.visitedRegions;
|
||||
|
||||
let geoJSON = data.props.geoJSON;
|
||||
let geoJSON = [];
|
||||
|
||||
let visitArray = [];
|
||||
|
||||
|
@ -77,11 +95,29 @@
|
|||
}
|
||||
|
||||
// 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;
|
||||
</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}
|
||||
<button type="button" class="btn btn-primary mb-2" on:click={() => (createModalOpen = true)}
|
||||
>Add New Adventure at Marker</button
|
||||
|
@ -156,7 +192,7 @@
|
|||
{/if}
|
||||
{/each}
|
||||
{#if showGEO}
|
||||
<GeoJSON id="states" data={data.props.geoJSON} promoteId="ISOCODE">
|
||||
<GeoJSON id="states" data={geoJSON} promoteId="ISOCODE">
|
||||
<LineLayer
|
||||
layout={{ 'line-cap': 'round', 'line-join': 'round' }}
|
||||
paint={{ 'line-color': 'grey', 'line-width': 3 }}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue