1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-07-23 14:59:36 +02:00

feat: Refactor hotel terminology to lodging and update related components

This commit is contained in:
Sean Morley 2025-02-08 16:10:01 -05:00
parent d2cb862103
commit 68924d7ecc
17 changed files with 510 additions and 135 deletions

View file

@ -0,0 +1,23 @@
# Generated by Django 5.0.8 on 2025-02-08 01:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('achievements', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='achievement',
name='key',
field=models.CharField(default='achievements.other', max_length=255, unique=True),
),
migrations.AddField(
model_name='achievement',
name='type',
field=models.CharField(choices=[('adventure_count', 'adventure_count'), ('country_count', 'country_count')], default='adventure_count', max_length=255),
),
]

View file

@ -1,3 +1,4 @@
import uuid
from django.db import models from django.db import models
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
@ -11,8 +12,8 @@ VALID_ACHIEVEMENT_TYPES = [
class Achievement(models.Model): class Achievement(models.Model):
"""Stores all possible achievements""" """Stores all possible achievements"""
name = models.CharField(max_length=255, unique=True) name = models.CharField(max_length=255, unique=True)
key = models.CharField(max_length=255, unique=True) # Used for frontend lookups, e.g. "achievements.first_adventure" key = models.CharField(max_length=255, unique=True, default='achievements.other') # Used for frontend lookups, e.g. "achievements.first_adventure"
type = models.CharField(max_length=255) # adventure_count, country_count, etc. type = models.CharField(max_length=255, choices=[(tag, tag) for tag in VALID_ACHIEVEMENT_TYPES], default='adventure_count') # adventure_count, country_count, etc.
description = models.TextField() description = models.TextField()
icon = models.ImageField(upload_to="achievements/", null=True, blank=True) icon = models.ImageField(upload_to="achievements/", null=True, blank=True)
condition = models.JSONField() # Stores rules like {"type": "adventure_count", "value": 10} condition = models.JSONField() # Stores rules like {"type": "adventure_count", "value": 10}

View file

@ -1,7 +1,7 @@
import os import os
from django.contrib import admin from django.contrib import admin
from django.utils.html import mark_safe from django.utils.html import mark_safe
from .models import Adventure, Checklist, ChecklistItem, Collection, Transportation, Note, AdventureImage, Visit, Category, Attachment, Hotel from .models import Adventure, Checklist, ChecklistItem, Collection, Transportation, Note, AdventureImage, Visit, Category, Attachment, Lodging
from worldtravel.models import Country, Region, VisitedRegion, City, VisitedCity from worldtravel.models import Country, Region, VisitedRegion, City, VisitedCity
from allauth.account.decorators import secure_admin_login from allauth.account.decorators import secure_admin_login
@ -140,7 +140,7 @@ admin.site.register(Category, CategoryAdmin)
admin.site.register(City, CityAdmin) admin.site.register(City, CityAdmin)
admin.site.register(VisitedCity) admin.site.register(VisitedCity)
admin.site.register(Attachment) admin.site.register(Attachment)
admin.site.register(Hotel) admin.site.register(Lodging)
admin.site.site_header = 'AdventureLog Admin' admin.site.site_header = 'AdventureLog Admin'
admin.site.site_title = 'AdventureLog Admin Site' admin.site.site_title = 'AdventureLog Admin Site'

View file

@ -0,0 +1,43 @@
# Generated by Django 5.0.8 on 2025-02-08 01:50
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('adventures', '0022_hotel'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Lodging',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('name', models.CharField(max_length=200)),
('type', models.CharField(choices=[('hotel', 'Hotel'), ('hostel', 'Hostel'), ('resort', 'Resort'), ('bnb', 'Bed & Breakfast'), ('campground', 'Campground'), ('cabin', 'Cabin'), ('apartment', 'Apartment'), ('house', 'House'), ('villa', 'Villa'), ('motel', 'Motel'), ('other', 'Other')], default='other', max_length=100)),
('description', models.TextField(blank=True, null=True)),
('rating', models.FloatField(blank=True, null=True)),
('link', models.URLField(blank=True, max_length=2083, null=True)),
('check_in', models.DateTimeField(blank=True, null=True)),
('check_out', models.DateTimeField(blank=True, null=True)),
('reservation_number', models.CharField(blank=True, max_length=100, null=True)),
('price', models.DecimalField(blank=True, decimal_places=2, max_digits=9, null=True)),
('latitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)),
('longitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)),
('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)),
],
),
migrations.DeleteModel(
name='Hotel',
),
]

View file

@ -36,6 +36,20 @@ ADVENTURE_TYPES = [
('other', 'Other') ('other', 'Other')
] ]
LODGING_TYPES = [
('hotel', 'Hotel'),
('hostel', 'Hostel'),
('resort', 'Resort'),
('bnb', 'Bed & Breakfast'),
('campground', 'Campground'),
('cabin', 'Cabin'),
('apartment', 'Apartment'),
('house', 'House'),
('villa', 'Villa'),
('motel', 'Motel'),
('other', 'Other')
]
TRANSPORTATION_TYPES = [ TRANSPORTATION_TYPES = [
('car', 'Car'), ('car', 'Car'),
('plane', 'Plane'), ('plane', 'Plane'),
@ -320,11 +334,12 @@ class Category(models.Model):
def __str__(self): def __str__(self):
return self.name + ' - ' + self.display_name + ' - ' + self.icon return self.name + ' - ' + self.display_name + ' - ' + self.icon
class Hotel(models.Model): class Lodging(models.Model):
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True) id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
user_id = models.ForeignKey( user_id = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user_id) User, on_delete=models.CASCADE, default=default_user_id)
name = models.CharField(max_length=200) name = models.CharField(max_length=200)
type = models.CharField(max_length=100, choices=LODGING_TYPES, default='other')
description = models.TextField(blank=True, null=True) description = models.TextField(blank=True, null=True)
rating = models.FloatField(blank=True, null=True) rating = models.FloatField(blank=True, null=True)
link = models.URLField(blank=True, null=True, max_length=2083) link = models.URLField(blank=True, null=True, max_length=2083)
@ -346,9 +361,9 @@ class Hotel(models.Model):
if self.collection: if self.collection:
if self.collection.is_public and not self.is_public: if self.collection.is_public and not self.is_public:
raise ValidationError('Hotels associated with a public collection must be public. Collection: ' + self.collection.name + ' Hotel: ' + self.name) raise ValidationError('Lodging associated with a public collection must be public. Collection: ' + self.collection.name + ' Loging: ' + self.name)
if self.user_id != self.collection.user_id: if self.user_id != self.collection.user_id:
raise ValidationError('Hotels must be associated with collections owned by the same user. Collection owner: ' + self.collection.user_id.username + ' Hotel owner: ' + self.user_id.username) raise ValidationError('Lodging must be associated with collections owned by the same user. Collection owner: ' + self.collection.user_id.username + ' Lodging owner: ' + self.user_id.username)
def __str__(self): def __str__(self):
return self.name return self.name

View file

@ -1,6 +1,6 @@
from django.utils import timezone from django.utils import timezone
import os import os
from .models import Adventure, AdventureImage, ChecklistItem, Collection, Note, Transportation, Checklist, Visit, Category, Attachment, Hotel from .models import Adventure, AdventureImage, ChecklistItem, Collection, Note, Transportation, Checklist, Visit, Category, Attachment, Lodging
from rest_framework import serializers from rest_framework import serializers
from main.utils import CustomModelSerializer from main.utils import CustomModelSerializer
from users.serializers import CustomUserDetailsSerializer from users.serializers import CustomUserDetailsSerializer
@ -203,14 +203,14 @@ class TransportationSerializer(CustomModelSerializer):
] ]
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id'] read_only_fields = ['id', 'created_at', 'updated_at', 'user_id']
class HotelSerializer(CustomModelSerializer): class LodgingSerializer(CustomModelSerializer):
class Meta: class Meta:
model = Hotel model = Lodging
fields = [ fields = [
'id', 'user_id', 'name', 'description', 'rating', 'link', 'check_in', 'check_out', 'id', 'user_id', 'name', 'description', 'rating', 'link', 'check_in', 'check_out',
'reservation_number', 'price', 'latitude', 'longitude', 'location', 'is_public', 'reservation_number', 'price', 'latitude', 'longitude', 'location', 'is_public',
'collection', 'created_at', 'updated_at' 'collection', 'created_at', 'updated_at', 'type'
] ]
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id'] read_only_fields = ['id', 'created_at', 'updated_at', 'user_id']
@ -300,11 +300,11 @@ class CollectionSerializer(CustomModelSerializer):
transportations = TransportationSerializer(many=True, read_only=True, source='transportation_set') transportations = TransportationSerializer(many=True, read_only=True, source='transportation_set')
notes = NoteSerializer(many=True, read_only=True, source='note_set') notes = NoteSerializer(many=True, read_only=True, source='note_set')
checklists = ChecklistSerializer(many=True, read_only=True, source='checklist_set') checklists = ChecklistSerializer(many=True, read_only=True, source='checklist_set')
hotels = HotelSerializer(many=True, read_only=True, source='hotel_set') lodging = LodgingSerializer(many=True, read_only=True, source='lodging_set')
class Meta: class Meta:
model = Collection model = Collection
fields = ['id', 'description', 'user_id', 'name', 'is_public', 'adventures', 'created_at', 'start_date', 'end_date', 'transportations', 'notes', 'updated_at', 'checklists', 'is_archived', 'shared_with', 'link', 'hotels'] fields = ['id', 'description', 'user_id', 'name', 'is_public', 'adventures', 'created_at', 'start_date', 'end_date', 'transportations', 'notes', 'updated_at', 'checklists', 'is_archived', 'shared_with', 'link', 'lodging']
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id'] read_only_fields = ['id', 'created_at', 'updated_at', 'user_id']
def to_representation(self, instance): def to_representation(self, instance):

View file

@ -18,7 +18,7 @@ router.register(r'ics-calendar', IcsCalendarGeneratorViewSet, basename='ics-cale
router.register(r'overpass', OverpassViewSet, basename='overpass') router.register(r'overpass', OverpassViewSet, basename='overpass')
router.register(r'search', GlobalSearchView, basename='search') router.register(r'search', GlobalSearchView, basename='search')
router.register(r'attachments', AttachmentViewSet, basename='attachments') router.register(r'attachments', AttachmentViewSet, basename='attachments')
router.register(r'hotels', HotelViewSet, basename='hotels') router.register(r'lodging', LodgingViewSet, basename='lodging')
urlpatterns = [ urlpatterns = [

View file

@ -13,4 +13,4 @@ from .stats_view import *
from .transportation_view import * from .transportation_view import *
from .global_search_view import * from .global_search_view import *
from .attachment_view import * from .attachment_view import *
from .hotel_view import * from .lodging_view import *

View file

@ -2,21 +2,21 @@ from rest_framework import viewsets, status
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
from django.db.models import Q from django.db.models import Q
from adventures.models import Hotel from adventures.models import Lodging
from adventures.serializers import HotelSerializer from adventures.serializers import LodgingSerializer
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from adventures.permissions import IsOwnerOrSharedWithFullAccess from adventures.permissions import IsOwnerOrSharedWithFullAccess
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
class HotelViewSet(viewsets.ModelViewSet): class LodgingViewSet(viewsets.ModelViewSet):
queryset = Hotel.objects.all() queryset = Lodging.objects.all()
serializer_class = HotelSerializer serializer_class = LodgingSerializer
permission_classes = [IsOwnerOrSharedWithFullAccess] permission_classes = [IsOwnerOrSharedWithFullAccess]
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
if not request.user.is_authenticated: if not request.user.is_authenticated:
return Response(status=status.HTTP_403_FORBIDDEN) return Response(status=status.HTTP_403_FORBIDDEN)
queryset = Hotel.objects.filter( queryset = Lodging.objects.filter(
Q(user_id=request.user.id) Q(user_id=request.user.id)
) )
serializer = self.get_serializer(queryset, many=True) serializer = self.get_serializer(queryset, many=True)
@ -26,11 +26,11 @@ class HotelViewSet(viewsets.ModelViewSet):
user = self.request.user user = self.request.user
if self.action == 'retrieve': if self.action == 'retrieve':
# For individual adventure retrieval, include public adventures, user's own adventures and shared adventures # For individual adventure retrieval, include public adventures, user's own adventures and shared adventures
return Hotel.objects.filter( return Lodging.objects.filter(
Q(is_public=True) | Q(user_id=user.id) | Q(collection__shared_with=user.id) Q(is_public=True) | Q(user_id=user.id) | Q(collection__shared_with=user.id)
).distinct().order_by('-updated_at') ).distinct().order_by('-updated_at')
# For other actions, include user's own adventures and shared adventures # For other actions, include user's own adventures and shared adventures
return Hotel.objects.filter( return Lodging.objects.filter(
Q(user_id=user.id) | Q(collection__shared_with=user.id) Q(user_id=user.id) | Q(collection__shared_with=user.id)
).distinct().order_by('-updated_at') ).distinct().order_by('-updated_at')

View file

@ -178,6 +178,8 @@ export default defineConfig({
{ icon: "github", link: "https://github.com/seanmorley15/AdventureLog" }, { icon: "github", link: "https://github.com/seanmorley15/AdventureLog" },
{ icon: "discord", link: "https://discord.gg/wRbQ9Egr8C" }, { icon: "discord", link: "https://discord.gg/wRbQ9Egr8C" },
{ icon: "buymeacoffee", link: "https://buymeacoffee.com/seanmorley15" }, { icon: "buymeacoffee", link: "https://buymeacoffee.com/seanmorley15" },
{ icon: "x", link: "https://x.com/AdventureLogApp" },
{ icon: "mastodon", link: "https://mastodon.social/@adventurelog" },
], ],
}, },
}); });

View file

@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import { appVersion } from '$lib/config'; import { appVersion } from '$lib/config';
import { addToast } from '$lib/toasts'; import { addToast } from '$lib/toasts';
import type { Adventure, Hotel, OpenStreetMapPlace, Point, ReverseGeocode } from '$lib/types'; import type { Adventure, Lodging, OpenStreetMapPlace, Point, ReverseGeocode } from '$lib/types';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { DefaultMarker, MapEvents, MapLibre } from 'svelte-maplibre'; import { DefaultMarker, MapEvents, MapLibre } from 'svelte-maplibre';
export let item: Adventure | Hotel; export let item: Adventure | Lodging;
export let triggerMarkVisted: boolean = false; export let triggerMarkVisted: boolean = false;
let reverseGeocodePlace: ReverseGeocode | null = null; let reverseGeocodePlace: ReverseGeocode | null = null;
@ -279,40 +279,39 @@ it would also work to just use on:click on the MapLibre component itself. -->
{/each} {/each}
</MapLibre> </MapLibre>
{#if reverseGeocodePlace} {#if reverseGeocodePlace}
<div class="mt-2"> <div class="mt-2 p-4 bg-neutral rounded-lg shadow-md">
<p> <h3 class="text-lg font-bold mb-2">{$t('adventures.location_details')}</h3>
<p class="mb-1">
<span class="font-semibold">{$t('adventures.display_name')}:</span>
{reverseGeocodePlace.city {reverseGeocodePlace.city
? reverseGeocodePlace.city + ', ' ? reverseGeocodePlace.city + ', '
: ''}{reverseGeocodePlace.region}, : ''}{reverseGeocodePlace.region}, {reverseGeocodePlace.country}
{reverseGeocodePlace.country}
</p> </p>
<p> <p class="mb-1">
{reverseGeocodePlace.region}: <span class="font-semibold">{$t('adventures.region')}:</span>
{reverseGeocodePlace.region_visited {reverseGeocodePlace.region}
? $t('adventures.visited') {reverseGeocodePlace.region_visited ? '✅' : '❌'}
: $t('adventures.not_visited')}
</p> </p>
{#if reverseGeocodePlace.city} {#if reverseGeocodePlace.city}
<p> <p class="mb-1">
{reverseGeocodePlace.city}: <span class="font-semibold">{$t('adventures.city')}:</span>
{reverseGeocodePlace.city_visited {reverseGeocodePlace.city}
? $t('adventures.visited') {reverseGeocodePlace.city_visited ? '✅' : '❌'}
: $t('adventures.not_visited')}
</p> </p>
{/if} {/if}
</div> </div>
{#if !reverseGeocodePlace.region_visited || (!reverseGeocodePlace.city_visited && !willBeMarkedVisited)} {#if !reverseGeocodePlace.region_visited || (!reverseGeocodePlace.city_visited && !willBeMarkedVisited)}
<button type="button" class="btn btn-neutral" on:click={markVisited}> <button type="button" class="btn btn-primary mt-2" on:click={markVisited}>
{$t('adventures.mark_visited')} {$t('adventures.mark_visited')}
</button> </button>
{/if} {/if}
{#if (willBeMarkedVisited && !reverseGeocodePlace.region_visited && reverseGeocodePlace.region_id) || (!reverseGeocodePlace.city_visited && willBeMarkedVisited && reverseGeocodePlace.city_id)} {#if (willBeMarkedVisited && !reverseGeocodePlace.region_visited && reverseGeocodePlace.region_id) || (!reverseGeocodePlace.city_visited && willBeMarkedVisited && reverseGeocodePlace.city_id)}
<div role="alert" class="alert alert-info mt-2"> <div role="alert" class="alert alert-info mt-2 flex items-center">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
class="h-6 w-6 shrink-0 stroke-current" class="h-6 w-6 shrink-0 stroke-current mr-2"
> >
<path <path
stroke-linecap="round" stroke-linecap="round"
@ -321,13 +320,12 @@ it would also work to just use on:click on the MapLibre component itself. -->
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path> ></path>
</svg> </svg>
<span <span>
>{reverseGeocodePlace.city {reverseGeocodePlace.city
? reverseGeocodePlace.city + ', ' ? reverseGeocodePlace.city + ', '
: ''}{reverseGeocodePlace.region}, : ''}{reverseGeocodePlace.region}, {reverseGeocodePlace.country}
{reverseGeocodePlace.country} {$t('adventures.will_be_marked')}
{$t('adventures.will_be_marked')}</span </span>
>
</div> </div>
{/if} {/if}
{/if} {/if}

View file

@ -0,0 +1,176 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import TrashCanOutline from '~icons/mdi/trash-can-outline';
import FileDocumentEdit from '~icons/mdi/file-document-edit';
import type { Collection, Lodging, User } from '$lib/types';
import { addToast } from '$lib/toasts';
import { t } from 'svelte-i18n';
import DeleteWarning from './DeleteWarning.svelte';
// import ArrowDownThick from '~icons/mdi/arrow-down-thick';
const dispatch = createEventDispatcher();
export let lodging: Lodging;
export let user: User | null = null;
export let collection: Collection | null = null;
let isWarningModalOpen: boolean = false;
function editTransportation() {
dispatch('edit', lodging);
}
let unlinked: boolean = false;
$: {
if (collection?.start_date && collection.end_date) {
// Parse transportation dates
let transportationStartDate = lodging.check_in
? new Date(lodging.check_in.split('T')[0]) // Ensure proper date parsing
: null;
let transportationEndDate = lodging.check_out
? new Date(lodging.check_out.split('T')[0])
: null;
// Parse collection dates
let collectionStartDate = new Date(collection.start_date);
let collectionEndDate = new Date(collection.end_date);
// // Debugging outputs
// console.log(
// 'Transportation Start Date:',
// transportationStartDate,
// 'Transportation End Date:',
// transportationEndDate
// );
// console.log(
// 'Collection Start Date:',
// collectionStartDate,
// 'Collection End Date:',
// collectionEndDate
// );
// Check if the collection range is outside the transportation range
const startOutsideRange =
transportationStartDate &&
collectionStartDate < transportationStartDate &&
collectionEndDate < transportationStartDate;
const endOutsideRange =
transportationEndDate &&
collectionStartDate > transportationEndDate &&
collectionEndDate > transportationEndDate;
unlinked = !!(
startOutsideRange ||
endOutsideRange ||
(!transportationStartDate && !transportationEndDate)
);
}
}
async function deleteTransportation() {
let res = await fetch(`/api/lodging/${lodging.id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
});
if (!res.ok) {
console.log($t('transportation.transportation_delete_error'));
} else {
addToast('info', $t('transportation.transportation_deleted'));
isWarningModalOpen = false;
dispatch('delete', lodging.id);
}
}
</script>
{#if isWarningModalOpen}
<DeleteWarning
title={$t('adventures.delete_transportation')}
button_text="Delete"
description={$t('adventures.transportation_delete_confirm')}
is_warning={false}
on:close={() => (isWarningModalOpen = false)}
on:confirm={deleteTransportation}
/>
{/if}
<div
class="card w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-md xl:max-w-md bg-neutral text-neutral-content shadow-xl"
>
<div class="card-body space-y-4">
<!-- Title and Type -->
<div class="flex items-center justify-between">
<h2 class="card-title text-lg font-semibold truncate">{lodging.name}</h2>
<div class="flex items-center gap-2">
<div class="badge badge-secondary">
{lodging.type}
</div>
<!-- {#if hotel.type == 'plane' && hotel.flight_number}
<div class="badge badge-neutral-200">{hotel.flight_number}</div>
{/if} -->
</div>
</div>
{#if unlinked}
<div class="badge badge-error">{$t('adventures.out_of_range')}</div>
{/if}
<!-- Locations -->
<div class="space-y-2">
{#if lodging.location}
<div class="flex items-center gap-2">
<span class="font-medium text-sm">{$t('adventures.from')}:</span>
<p class="break-words">{lodging.location}</p>
</div>
{/if}
{#if lodging.check_in && lodging.check_out}
<div class="flex items-center gap-2">
<span class="font-medium text-sm">{$t('adventures.start')}:</span>
<p>{new Date(lodging.check_in).toLocaleDateString(undefined, { timeZone: 'UTC' })}</p>
</div>
{/if}
</div>
<!-- Dates -->
<div class="space-y-2">
{#if lodging.location}
<!-- <ArrowDownThick class="w-4 h-4" /> -->
<div class="flex items-center gap-2">
<span class="font-medium text-sm">{$t('adventures.to')}:</span>
<p class="break-words">{lodging.location}</p>
</div>
{/if}
{#if lodging.check_out}
<div class="flex items-center gap-2">
<span class="font-medium text-sm">{$t('adventures.end')}:</span>
<p>{new Date(lodging.check_out).toLocaleDateString(undefined, { timeZone: 'UTC' })}</p>
</div>
{/if}
</div>
<!-- Actions -->
{#if lodging.user_id == user?.uuid || (collection && user && collection.shared_with && collection.shared_with.includes(user.uuid))}
<div class="card-actions justify-end">
<button
class="btn btn-primary btn-sm flex items-center gap-1"
on:click={editTransportation}
title="Edit"
>
<FileDocumentEdit class="w-5 h-5" />
<span>{$t('transportation.edit')}</span>
</button>
<button
on:click={() => (isWarningModalOpen = true)}
class="btn btn-secondary btn-sm flex items-center gap-1"
title="Delete"
>
<TrashCanOutline class="w-5 h-5" />
<span>{$t('adventures.delete')}</span>
</button>
</div>
{/if}
</div>
</div>

View file

@ -3,27 +3,19 @@
import { addToast } from '$lib/toasts'; import { addToast } from '$lib/toasts';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import MarkdownEditor from './MarkdownEditor.svelte'; import MarkdownEditor from './MarkdownEditor.svelte';
import { appVersion } from '$lib/config'; import type { Collection, Lodging } from '$lib/types';
import { DefaultMarker, MapEvents, MapLibre } from 'svelte-maplibre';
import type { Collection, Hotel, ReverseGeocode, OpenStreetMapPlace, Point } from '$lib/types';
import LocationDropdown from './LocationDropdown.svelte'; import LocationDropdown from './LocationDropdown.svelte';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
export let collection: Collection; export let collection: Collection;
export let hotelToEdit: Hotel | null = null; export let lodgingToEdit: Lodging | null = null;
let modal: HTMLDialogElement; let modal: HTMLDialogElement;
let constrainDates: boolean = false; let constrainDates: boolean = false;
let hotel: Hotel = { ...initializeHotel(hotelToEdit) }; let lodging: Lodging = { ...initializeLodging(lodgingToEdit) };
let fullStartDate: string = ''; let fullStartDate: string = '';
let fullEndDate: string = ''; let fullEndDate: string = '';
let reverseGeocodePlace: any | null = null;
let query: string = '';
let places: OpenStreetMapPlace[] = [];
let noPlaces: boolean = false;
let is_custom_location: boolean = false;
let markers: Point[] = [];
// Format date as local datetime // Format date as local datetime
function toLocalDatetime(value: string | null): string { function toLocalDatetime(value: string | null): string {
@ -32,12 +24,32 @@
return date.toISOString().slice(0, 16); // Format: YYYY-MM-DDTHH:mm return date.toISOString().slice(0, 16); // Format: YYYY-MM-DDTHH:mm
} }
type LodgingType = {
value: string;
label: string;
};
const LODGING_TYPES: LodgingType[] = [
{ value: 'hotel', label: 'Hotel' },
{ value: 'hostel', label: 'Hostel' },
{ value: 'resort', label: 'Resort' },
{ value: 'bnb', label: 'Bed & Breakfast' },
{ value: 'campground', label: 'Campground' },
{ value: 'cabin', label: 'Cabin' },
{ value: 'apartment', label: 'Apartment' },
{ value: 'house', label: 'House' },
{ value: 'villa', label: 'Villa' },
{ value: 'motel', label: 'Motel' },
{ value: 'other', label: 'Other' }
];
// Initialize hotel with values from hotelToEdit or default values // Initialize hotel with values from hotelToEdit or default values
function initializeHotel(hotelToEdit: Hotel | null): Hotel { function initializeLodging(hotelToEdit: Lodging | null): Lodging {
return { return {
id: hotelToEdit?.id || '', id: hotelToEdit?.id || '',
user_id: hotelToEdit?.user_id || '', user_id: hotelToEdit?.user_id || '',
name: hotelToEdit?.name || '', name: hotelToEdit?.name || '',
type: hotelToEdit?.type || 'other',
description: hotelToEdit?.description || '', description: hotelToEdit?.description || '',
rating: hotelToEdit?.rating || NaN, rating: hotelToEdit?.rating || NaN,
link: hotelToEdit?.link || '', link: hotelToEdit?.link || '',
@ -49,7 +61,7 @@
longitude: hotelToEdit?.longitude || null, longitude: hotelToEdit?.longitude || null,
location: hotelToEdit?.location || '', location: hotelToEdit?.location || '',
is_public: hotelToEdit?.is_public || false, is_public: hotelToEdit?.is_public || false,
collection: hotelToEdit?.collection || '', collection: hotelToEdit?.collection || collection.id,
created_at: hotelToEdit?.created_at || '', created_at: hotelToEdit?.created_at || '',
updated_at: hotelToEdit?.updated_at || '' updated_at: hotelToEdit?.updated_at || ''
}; };
@ -63,8 +75,8 @@
// Handle rating change // Handle rating change
$: { $: {
if (!hotel.rating) { if (!lodging.rating) {
hotel.rating = NaN; lodging.rating = NaN;
} }
} }
@ -88,35 +100,37 @@
async function handleSubmit(event: Event) { async function handleSubmit(event: Event) {
event.preventDefault(); event.preventDefault();
if (hotel.check_in && !hotel.check_out) { if (lodging.check_in && !lodging.check_out) {
const checkInDate = new Date(hotel.check_in); const checkInDate = new Date(lodging.check_in);
checkInDate.setDate(checkInDate.getDate() + 1); checkInDate.setDate(checkInDate.getDate() + 1);
hotel.check_out = checkInDate.toISOString(); lodging.check_out = checkInDate.toISOString();
} }
if (hotel.check_in && hotel.check_out && hotel.check_in > hotel.check_out) { if (lodging.check_in && lodging.check_out && lodging.check_in > lodging.check_out) {
addToast('error', $t('adventures.start_before_end_error')); addToast('error', $t('adventures.start_before_end_error'));
return; return;
} }
// Create or update hotel // Create or update hotel
const url = hotel.id === '' ? '/api/hotels' : `/api/hotels/${hotel.id}`; const url = lodging.id === '' ? '/api/lodging' : `/api/lodging/${lodging.id}`;
const method = hotel.id === '' ? 'POST' : 'PATCH'; const method = lodging.id === '' ? 'POST' : 'PATCH';
const res = await fetch(url, { const res = await fetch(url, {
method, method,
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(hotel) body: JSON.stringify(lodging)
}); });
const data = await res.json(); const data = await res.json();
if (data.id) { if (data.id) {
hotel = data as Hotel; lodging = data as Lodging;
const toastMessage = const toastMessage =
hotel.id === '' ? 'adventures.adventure_created' : 'adventures.adventure_updated'; lodging.id === '' ? 'adventures.adventure_created' : 'adventures.adventure_updated';
addToast('success', $t(toastMessage)); addToast('success', $t(toastMessage));
dispatch('save', hotel); dispatch('save', lodging);
} else { } else {
const errorMessage = const errorMessage =
hotel.id === '' ? 'adventures.adventure_create_error' : 'adventures.adventure_update_error'; lodging.id === ''
? 'adventures.adventure_create_error'
: 'adventures.adventure_update_error';
addToast('error', $t(errorMessage)); addToast('error', $t(errorMessage));
} }
} }
@ -127,9 +141,7 @@
<!-- svelte-ignore a11y-no-noninteractive-element-interactions --> <!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<div class="modal-box w-11/12 max-w-3xl" role="dialog" on:keydown={handleKeydown} tabindex="0"> <div class="modal-box w-11/12 max-w-3xl" role="dialog" on:keydown={handleKeydown} tabindex="0">
<h3 class="font-bold text-2xl"> <h3 class="font-bold text-2xl">
{hotelToEdit {lodgingToEdit ? $t('lodging.edit_lodging') : $t('lodging.new_lodging')}
? $t('transportation.edit_transportation')
: $t('transportation.new_transportation')}
</h3> </h3>
<div class="modal-action items-center"> <div class="modal-action items-center">
<form method="post" style="width: 100%;" on:submit={handleSubmit}> <form method="post" style="width: 100%;" on:submit={handleSubmit}>
@ -149,7 +161,7 @@
type="text" type="text"
id="name" id="name"
name="name" name="name"
bind:value={hotel.name} bind:value={lodging.name}
class="input input-bordered w-full" class="input input-bordered w-full"
required required
/> />
@ -157,7 +169,7 @@
<!-- Description --> <!-- Description -->
<div> <div>
<label for="description">{$t('adventures.description')}</label><br /> <label for="description">{$t('adventures.description')}</label><br />
<MarkdownEditor bind:text={hotel.description} editor_height={'h-32'} /> <MarkdownEditor bind:text={lodging.description} editor_height={'h-32'} />
</div> </div>
<!-- Rating --> <!-- Rating -->
<div> <div>
@ -167,7 +179,7 @@
min="0" min="0"
max="5" max="5"
hidden hidden
bind:value={hotel.rating} bind:value={lodging.rating}
id="rating" id="rating"
name="rating" name="rating"
class="input input-bordered w-full max-w-xs mt-1" class="input input-bordered w-full max-w-xs mt-1"
@ -177,48 +189,48 @@
type="radio" type="radio"
name="rating-2" name="rating-2"
class="rating-hidden" class="rating-hidden"
checked={Number.isNaN(hotel.rating)} checked={Number.isNaN(lodging.rating)}
/> />
<input <input
type="radio" type="radio"
name="rating-2" name="rating-2"
class="mask mask-star-2 bg-orange-400" class="mask mask-star-2 bg-orange-400"
on:click={() => (hotel.rating = 1)} on:click={() => (lodging.rating = 1)}
checked={hotel.rating === 1} checked={lodging.rating === 1}
/> />
<input <input
type="radio" type="radio"
name="rating-2" name="rating-2"
class="mask mask-star-2 bg-orange-400" class="mask mask-star-2 bg-orange-400"
on:click={() => (hotel.rating = 2)} on:click={() => (lodging.rating = 2)}
checked={hotel.rating === 2} checked={lodging.rating === 2}
/> />
<input <input
type="radio" type="radio"
name="rating-2" name="rating-2"
class="mask mask-star-2 bg-orange-400" class="mask mask-star-2 bg-orange-400"
on:click={() => (hotel.rating = 3)} on:click={() => (lodging.rating = 3)}
checked={hotel.rating === 3} checked={lodging.rating === 3}
/> />
<input <input
type="radio" type="radio"
name="rating-2" name="rating-2"
class="mask mask-star-2 bg-orange-400" class="mask mask-star-2 bg-orange-400"
on:click={() => (hotel.rating = 4)} on:click={() => (lodging.rating = 4)}
checked={hotel.rating === 4} checked={lodging.rating === 4}
/> />
<input <input
type="radio" type="radio"
name="rating-2" name="rating-2"
class="mask mask-star-2 bg-orange-400" class="mask mask-star-2 bg-orange-400"
on:click={() => (hotel.rating = 5)} on:click={() => (lodging.rating = 5)}
checked={hotel.rating === 5} checked={lodging.rating === 5}
/> />
{#if hotel.rating} {#if lodging.rating}
<button <button
type="button" type="button"
class="btn btn-sm btn-error ml-2" class="btn btn-sm btn-error ml-2"
on:click={() => (hotel.rating = NaN)} on:click={() => (lodging.rating = NaN)}
> >
{$t('adventures.remove')} {$t('adventures.remove')}
</button> </button>
@ -232,7 +244,7 @@
type="url" type="url"
id="link" id="link"
name="link" name="link"
bind:value={hotel.link} bind:value={lodging.link}
class="input input-bordered w-full" class="input input-bordered w-full"
/> />
</div> </div>
@ -247,7 +259,7 @@
<!-- Start Date --> <!-- Start Date -->
<div> <div>
<label for="date"> <label for="date">
{$t('adventures.start_date')} {$t('lodging.check_in')}
</label> </label>
{#if collection && collection.start_date && collection.end_date}<label {#if collection && collection.start_date && collection.end_date}<label
@ -268,7 +280,7 @@
type="datetime-local" type="datetime-local"
id="date" id="date"
name="date" name="date"
bind:value={hotel.check_in} bind:value={lodging.check_in}
min={constrainDates ? fullStartDate : ''} min={constrainDates ? fullStartDate : ''}
max={constrainDates ? fullEndDate : ''} max={constrainDates ? fullEndDate : ''}
class="input input-bordered w-full max-w-xs mt-1" class="input input-bordered w-full max-w-xs mt-1"
@ -276,19 +288,19 @@
</div> </div>
</div> </div>
<!-- End Date --> <!-- End Date -->
{#if hotel.check_in} {#if lodging.check_out}
<div> <div>
<label for="end_date"> <label for="end_date">
{$t('adventures.end_date')} {$t('lodging.check_out')}
</label> </label>
<div> <div>
<input <input
type="datetime-local" type="datetime-local"
id="end_date" id="end_date"
name="end_date" name="end_date"
min={constrainDates ? hotel.check_in : ''} min={constrainDates ? lodging.check_in : ''}
max={constrainDates ? fullEndDate : ''} max={constrainDates ? fullEndDate : ''}
bind:value={hotel.check_out} bind:value={lodging.check_out}
class="input input-bordered w-full max-w-xs mt-1" class="input input-bordered w-full max-w-xs mt-1"
/> />
</div> </div>
@ -298,7 +310,7 @@
</div> </div>
<!-- Location Information --> <!-- Location Information -->
<LocationDropdown bind:item={hotel} /> <LocationDropdown bind:item={lodging} />
<!-- Form Actions --> <!-- Form Actions -->
<div class="mt-4"> <div class="mt-4">

View file

@ -113,7 +113,7 @@ export type Collection = {
end_date: string | null; end_date: string | null;
transportations?: Transportation[]; transportations?: Transportation[];
notes?: Note[]; notes?: Note[];
hotels?: Hotel[]; lodging?: Lodging[];
checklists?: Checklist[]; checklists?: Checklist[];
is_archived?: boolean; is_archived?: boolean;
shared_with: string[] | undefined; shared_with: string[] | undefined;
@ -264,10 +264,11 @@ export type Attachment = {
name: string; name: string;
}; };
export type Hotel = { export type Lodging = {
id: string; id: string;
user_id: string; user_id: string;
name: string; name: string;
type: string;
description: string | null; description: string | null;
rating: number | null; rating: number | null;
link: string | null; link: string | null;

View file

@ -235,7 +235,12 @@
"primary": "Primär", "primary": "Primär",
"upload": "Hochladen", "upload": "Hochladen",
"view_attachment": "Anhang anzeigen", "view_attachment": "Anhang anzeigen",
"of": "von" "of": "von",
"city": "Stadt",
"display_name": "Anzeigename",
"location_details": "Standortdetails",
"lodging": "Unterkunft",
"region": "Region"
}, },
"home": { "home": {
"desc_1": "Entdecken, planen und erkunden Sie mit Leichtigkeit", "desc_1": "Entdecken, planen und erkunden Sie mit Leichtigkeit",

View file

@ -191,7 +191,7 @@
"no_description_found": "No description found", "no_description_found": "No description found",
"adventure_created": "Adventure created", "adventure_created": "Adventure created",
"adventure_create_error": "Failed to create adventure", "adventure_create_error": "Failed to create adventure",
"hotel": "Hotel", "lodging": "Lodging",
"create_adventure": "Create Adventure", "create_adventure": "Create Adventure",
"adventure_updated": "Adventure updated", "adventure_updated": "Adventure updated",
"adventure_update_error": "Failed to update adventure", "adventure_update_error": "Failed to update adventure",
@ -200,6 +200,7 @@
"new_adventure": "New Adventure", "new_adventure": "New Adventure",
"basic_information": "Basic Information", "basic_information": "Basic Information",
"no_adventures_to_recommendations": "No adventures found. Add at leat one adventure to get recommendations.", "no_adventures_to_recommendations": "No adventures found. Add at leat one adventure to get recommendations.",
"display_name": "Display Name",
"adventure_not_found": "There are no adventures to display. Add some using the plus button at the bottom right or try changing filters!", "adventure_not_found": "There are no adventures to display. Add some using the plus button at the bottom right or try changing filters!",
"no_adventures_found": "No adventures found", "no_adventures_found": "No adventures found",
"mark_region_as_visited": "Mark region {region}, {country} as visited?", "mark_region_as_visited": "Mark region {region}, {country} as visited?",
@ -250,6 +251,9 @@
"out_of_range": "Not in itinerary date range", "out_of_range": "Not in itinerary date range",
"preview": "Preview", "preview": "Preview",
"finding_recommendations": "Discovering hidden gems for your next adventure", "finding_recommendations": "Discovering hidden gems for your next adventure",
"location_details": "Location Details",
"city": "City",
"region": "Region",
"md_instructions": "Write your markdown here...", "md_instructions": "Write your markdown here...",
"days": "days", "days": "days",
"attachment_upload_success": "Attachment uploaded successfully!", "attachment_upload_success": "Attachment uploaded successfully!",
@ -489,6 +493,30 @@
"start": "Start", "start": "Start",
"date_and_time": "Date & Time" "date_and_time": "Date & Time"
}, },
"lodging": {
"lodging_deleted": "Lodging deleted successfully!",
"lodging_delete_error": "Error deleting lodging",
"provide_start_date": "Please provide a start date",
"lodging_type": "Lodging Type",
"type": "Type",
"lodging_added": "Lodging added successfully!",
"error_editing_lodging": "Error editing lodging",
"new_lodging": "New Lodging",
"check_in": "Check In",
"check_out": "Check Out",
"edit": "Edit",
"modes": {
"hotel": "Hotel",
"hostel": "Hostel",
"airbnb": "Airbnb",
"camping": "Camping",
"other": "Other"
},
"lodging_edit_success": "Lodging edited successfully!",
"edit_lodging": "Edit Lodging",
"start": "Start",
"date_and_time": "Date & Time"
},
"search": { "search": {
"adventurelog_results": "AdventureLog Results", "adventurelog_results": "AdventureLog Results",
"public_adventures": "Public Adventures", "public_adventures": "Public Adventures",

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import type { Adventure, Checklist, Collection, Hotel, Note, Transportation } from '$lib/types'; import type { Adventure, Checklist, Collection, Lodging, Note, Transportation } from '$lib/types';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
import { marked } from 'marked'; // Import the markdown parser import { marked } from 'marked'; // Import the markdown parser
@ -35,7 +35,8 @@
import TransportationModal from '$lib/components/TransportationModal.svelte'; import TransportationModal from '$lib/components/TransportationModal.svelte';
import CardCarousel from '$lib/components/CardCarousel.svelte'; import CardCarousel from '$lib/components/CardCarousel.svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import HotelModal from '$lib/components/HotelModal.svelte'; import LodgingModal from '$lib/components/LodgingModal.svelte';
import LodgingCard from '$lib/components/LodgingCard.svelte';
export let data: PageData; export let data: PageData;
console.log(data); console.log(data);
@ -104,6 +105,19 @@
); );
} }
if (lodging) {
dates = dates.concat(
lodging
.filter((i) => i.check_in)
.map((lodging) => ({
id: lodging.id,
start: lodging.check_in || '', // Ensure it's a string
end: lodging.check_out || lodging.check_in || '', // Ensure it's a string
title: lodging.name
}))
);
}
// Update `options.events` when `dates` changes // Update `options.events` when `dates` changes
options = { ...options, events: dates }; options = { ...options, events: dates };
} }
@ -116,7 +130,7 @@
let numAdventures: number = 0; let numAdventures: number = 0;
let transportations: Transportation[] = []; let transportations: Transportation[] = [];
let hotels: Hotel[] = []; let lodging: Lodging[] = [];
let notes: Note[] = []; let notes: Note[] = [];
let checklists: Checklist[] = []; let checklists: Checklist[] = [];
@ -176,8 +190,8 @@
if (collection.transportations) { if (collection.transportations) {
transportations = collection.transportations; transportations = collection.transportations;
} }
if (collection.hotels) { if (collection.lodging) {
hotels = collection.hotels; lodging = collection.lodging;
} }
if (collection.notes) { if (collection.notes) {
notes = collection.notes; notes = collection.notes;
@ -248,8 +262,8 @@
let adventureToEdit: Adventure | null = null; let adventureToEdit: Adventure | null = null;
let transportationToEdit: Transportation | null = null; let transportationToEdit: Transportation | null = null;
let isShowingHotelModal: boolean = false; let isShowingLodgingModal: boolean = false;
let hotelToEdit: Hotel | null = null; let lodgingToEdit: Lodging | null = null;
let isAdventureModalOpen: boolean = false; let isAdventureModalOpen: boolean = false;
let isNoteModalOpen: boolean = false; let isNoteModalOpen: boolean = false;
let noteToEdit: Note | null; let noteToEdit: Note | null;
@ -267,9 +281,9 @@
isShowingTransportationModal = true; isShowingTransportationModal = true;
} }
function editHotel(event: CustomEvent<Hotel>) { function editLodging(event: CustomEvent<Lodging>) {
hotelToEdit = event.detail; lodgingToEdit = event.detail;
isShowingHotelModal = true; isShowingLodgingModal = true;
} }
function saveOrCreateAdventure(event: CustomEvent<Adventure>) { function saveOrCreateAdventure(event: CustomEvent<Adventure>) {
@ -368,20 +382,20 @@
isShowingTransportationModal = false; isShowingTransportationModal = false;
} }
function saveOrCreateHotel(event: CustomEvent<Hotel>) { function saveOrCreateLodging(event: CustomEvent<Lodging>) {
if (hotels.find((hotel) => hotel.id === event.detail.id)) { if (lodging.find((lodging) => lodging.id === event.detail.id)) {
// Update existing hotel // Update existing hotel
hotels = hotels.map((hotel) => { lodging = lodging.map((lodging) => {
if (hotel.id === event.detail.id) { if (lodging.id === event.detail.id) {
return event.detail; return event.detail;
} }
return hotel; return lodging;
}); });
} else { } else {
// Create new hotel // Create new lodging
hotels = [event.detail, ...hotels]; lodging = [event.detail, ...lodging];
} }
isShowingHotelModal = false; isShowingLodgingModal = false;
} }
</script> </script>
@ -404,11 +418,11 @@
/> />
{/if} {/if}
{#if isShowingHotelModal} {#if isShowingLodgingModal}
<HotelModal <LodgingModal
{hotelToEdit} {lodgingToEdit}
on:close={() => (isShowingHotelModal = false)} on:close={() => (isShowingLodgingModal = false)}
on:save={saveOrCreateHotel} on:save={saveOrCreateLodging}
{collection} {collection}
/> />
{/if} {/if}
@ -541,12 +555,12 @@
<button <button
class="btn btn-primary" class="btn btn-primary"
on:click={() => { on:click={() => {
isShowingHotelModal = true; isShowingLodgingModal = true;
newType = ''; newType = '';
hotelToEdit = null; lodgingToEdit = null;
}} }}
> >
{$t('adventures.hotel')}</button {$t('adventures.lodging')}</button
> >
<!-- <button <!-- <button
@ -589,7 +603,7 @@
</div> </div>
{/if} {/if}
{#if collection && !collection.start_date && adventures.length == 0 && transportations.length == 0 && notes.length == 0 && checklists.length == 0} {#if collection && !collection.start_date && adventures.length == 0 && transportations.length == 0 && notes.length == 0 && checklists.length == 0 && lodging.length == 0}
<NotFound error={undefined} /> <NotFound error={undefined} />
{/if} {/if}
@ -701,6 +715,63 @@
</div> </div>
{/if} {/if}
{#if lodging.length > 0}
<h1 class="text-center font-bold text-4xl mt-4 mb-4">{$t('adventures.lodging')}</h1>
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
{#each lodging as hotel}
<LodgingCard
lodging={hotel}
user={data?.user}
on:delete={(event) => {
lodging = lodging.filter((t) => t.id != event.detail);
}}
on:edit={editLodging}
{collection}
/>
{/each}
</div>
{/if}
{#if notes.length > 0}
<h1 class="text-center font-bold text-4xl mt-4 mb-4">{$t('adventures.notes')}</h1>
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
{#each notes as note}
<NoteCard
{note}
user={data.user || null}
on:edit={(event) => {
noteToEdit = event.detail;
isNoteModalOpen = true;
}}
on:delete={(event) => {
notes = notes.filter((n) => n.id != event.detail);
}}
{collection}
/>
{/each}
</div>
{/if}
{#if checklists.length > 0}
<h1 class="text-center font-bold text-4xl mt-4 mb-4">{$t('adventures.checklists')}</h1>
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
{#each checklists as checklist}
<ChecklistCard
{checklist}
user={data.user || null}
on:delete={(event) => {
checklists = checklists.filter((n) => n.id != event.detail);
}}
on:edit={(event) => {
checklistToEdit = event.detail;
isShowingChecklistModal = true;
}}
{collection}
/>
{/each}
</div>
{/if}
{#if notes.length > 0} {#if notes.length > 0}
<h1 class="text-center font-bold text-4xl mt-4 mb-4">{$t('adventures.notes')}</h1> <h1 class="text-center font-bold text-4xl mt-4 mb-4">{$t('adventures.notes')}</h1>
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center"> <div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
@ -742,7 +813,7 @@
{/if} {/if}
<!-- if none found --> <!-- if none found -->
{#if adventures.length == 0 && transportations.length == 0 && notes.length == 0 && checklists.length == 0} {#if adventures.length == 0 && transportations.length == 0 && notes.length == 0 && checklists.length == 0 && lodging.length == 0}
<NotFound error={undefined} /> <NotFound error={undefined} />
{/if} {/if}
{/if} {/if}