1
0
Fork 0
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:
Sean Morley 2024-07-28 12:45:49 -04:00 committed by GitHub
commit 03ca0770df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 1273 additions and 121 deletions

View file

@ -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'

View file

@ -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),
),
]

View file

@ -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)),
],
),
]

View file

@ -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

View file

@ -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']

View file

@ -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 = [

View file

@ -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)

View file

@ -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'),
]

View file

@ -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)

View file

@ -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

View file

@ -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"

View file

@ -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">

View file

@ -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

View 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>

View file

@ -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>

View file

@ -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>

View 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>

View file

@ -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();
}

View 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>

View file

@ -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
};

View file

@ -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(

View file

@ -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, {

View file

@ -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');

View file

@ -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}

View file

@ -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
}
};

View file

@ -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 }}