mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-07-19 12:59:36 +02:00
feat: Refactor hotel terminology to lodging and update related components
This commit is contained in:
parent
d2cb862103
commit
68924d7ecc
17 changed files with 510 additions and 135 deletions
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -1,3 +1,4 @@
|
|||
import uuid
|
||||
from django.db import models
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
|
@ -11,8 +12,8 @@ VALID_ACHIEVEMENT_TYPES = [
|
|||
class Achievement(models.Model):
|
||||
"""Stores all possible achievements"""
|
||||
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"
|
||||
type = models.CharField(max_length=255) # adventure_count, country_count, etc.
|
||||
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, choices=[(tag, tag) for tag in VALID_ACHIEVEMENT_TYPES], default='adventure_count') # adventure_count, country_count, etc.
|
||||
description = models.TextField()
|
||||
icon = models.ImageField(upload_to="achievements/", null=True, blank=True)
|
||||
condition = models.JSONField() # Stores rules like {"type": "adventure_count", "value": 10}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import os
|
||||
from django.contrib import admin
|
||||
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 allauth.account.decorators import secure_admin_login
|
||||
|
||||
|
@ -140,7 +140,7 @@ admin.site.register(Category, CategoryAdmin)
|
|||
admin.site.register(City, CityAdmin)
|
||||
admin.site.register(VisitedCity)
|
||||
admin.site.register(Attachment)
|
||||
admin.site.register(Hotel)
|
||||
admin.site.register(Lodging)
|
||||
|
||||
admin.site.site_header = 'AdventureLog Admin'
|
||||
admin.site.site_title = 'AdventureLog Admin Site'
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
]
|
|
@ -36,6 +36,20 @@ ADVENTURE_TYPES = [
|
|||
('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 = [
|
||||
('car', 'Car'),
|
||||
('plane', 'Plane'),
|
||||
|
@ -320,11 +334,12 @@ class Category(models.Model):
|
|||
def __str__(self):
|
||||
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)
|
||||
user_id = models.ForeignKey(
|
||||
User, on_delete=models.CASCADE, default=default_user_id)
|
||||
name = models.CharField(max_length=200)
|
||||
type = models.CharField(max_length=100, choices=LODGING_TYPES, default='other')
|
||||
description = models.TextField(blank=True, null=True)
|
||||
rating = models.FloatField(blank=True, null=True)
|
||||
link = models.URLField(blank=True, null=True, max_length=2083)
|
||||
|
@ -346,9 +361,9 @@ class Hotel(models.Model):
|
|||
|
||||
if self.collection:
|
||||
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:
|
||||
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):
|
||||
return self.name
|
|
@ -1,6 +1,6 @@
|
|||
from django.utils import timezone
|
||||
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 main.utils import CustomModelSerializer
|
||||
from users.serializers import CustomUserDetailsSerializer
|
||||
|
@ -203,14 +203,14 @@ class TransportationSerializer(CustomModelSerializer):
|
|||
]
|
||||
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id']
|
||||
|
||||
class HotelSerializer(CustomModelSerializer):
|
||||
class LodgingSerializer(CustomModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Hotel
|
||||
model = Lodging
|
||||
fields = [
|
||||
'id', 'user_id', 'name', 'description', 'rating', 'link', 'check_in', 'check_out',
|
||||
'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']
|
||||
|
||||
|
@ -300,11 +300,11 @@ class CollectionSerializer(CustomModelSerializer):
|
|||
transportations = TransportationSerializer(many=True, read_only=True, source='transportation_set')
|
||||
notes = NoteSerializer(many=True, read_only=True, source='note_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:
|
||||
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']
|
||||
|
||||
def to_representation(self, instance):
|
||||
|
|
|
@ -18,7 +18,7 @@ router.register(r'ics-calendar', IcsCalendarGeneratorViewSet, basename='ics-cale
|
|||
router.register(r'overpass', OverpassViewSet, basename='overpass')
|
||||
router.register(r'search', GlobalSearchView, basename='search')
|
||||
router.register(r'attachments', AttachmentViewSet, basename='attachments')
|
||||
router.register(r'hotels', HotelViewSet, basename='hotels')
|
||||
router.register(r'lodging', LodgingViewSet, basename='lodging')
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
|
|
|
@ -13,4 +13,4 @@ from .stats_view import *
|
|||
from .transportation_view import *
|
||||
from .global_search_view import *
|
||||
from .attachment_view import *
|
||||
from .hotel_view import *
|
||||
from .lodging_view import *
|
|
@ -2,21 +2,21 @@ from rest_framework import viewsets, status
|
|||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from django.db.models import Q
|
||||
from adventures.models import Hotel
|
||||
from adventures.serializers import HotelSerializer
|
||||
from adventures.models import Lodging
|
||||
from adventures.serializers import LodgingSerializer
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from adventures.permissions import IsOwnerOrSharedWithFullAccess
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
class HotelViewSet(viewsets.ModelViewSet):
|
||||
queryset = Hotel.objects.all()
|
||||
serializer_class = HotelSerializer
|
||||
class LodgingViewSet(viewsets.ModelViewSet):
|
||||
queryset = Lodging.objects.all()
|
||||
serializer_class = LodgingSerializer
|
||||
permission_classes = [IsOwnerOrSharedWithFullAccess]
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
if not request.user.is_authenticated:
|
||||
return Response(status=status.HTTP_403_FORBIDDEN)
|
||||
queryset = Hotel.objects.filter(
|
||||
queryset = Lodging.objects.filter(
|
||||
Q(user_id=request.user.id)
|
||||
)
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
|
@ -26,11 +26,11 @@ class HotelViewSet(viewsets.ModelViewSet):
|
|||
user = self.request.user
|
||||
if self.action == 'retrieve':
|
||||
# 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)
|
||||
).distinct().order_by('-updated_at')
|
||||
# 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)
|
||||
).distinct().order_by('-updated_at')
|
||||
|
|
@ -178,6 +178,8 @@ export default defineConfig({
|
|||
{ icon: "github", link: "https://github.com/seanmorley15/AdventureLog" },
|
||||
{ icon: "discord", link: "https://discord.gg/wRbQ9Egr8C" },
|
||||
{ icon: "buymeacoffee", link: "https://buymeacoffee.com/seanmorley15" },
|
||||
{ icon: "x", link: "https://x.com/AdventureLogApp" },
|
||||
{ icon: "mastodon", link: "https://mastodon.social/@adventurelog" },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<script lang="ts">
|
||||
import { appVersion } from '$lib/config';
|
||||
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 { DefaultMarker, MapEvents, MapLibre } from 'svelte-maplibre';
|
||||
|
||||
export let item: Adventure | Hotel;
|
||||
export let item: Adventure | Lodging;
|
||||
export let triggerMarkVisted: boolean = false;
|
||||
|
||||
let reverseGeocodePlace: ReverseGeocode | null = null;
|
||||
|
@ -279,40 +279,39 @@ it would also work to just use on:click on the MapLibre component itself. -->
|
|||
{/each}
|
||||
</MapLibre>
|
||||
{#if reverseGeocodePlace}
|
||||
<div class="mt-2">
|
||||
<p>
|
||||
<div class="mt-2 p-4 bg-neutral rounded-lg shadow-md">
|
||||
<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.region},
|
||||
{reverseGeocodePlace.country}
|
||||
: ''}{reverseGeocodePlace.region}, {reverseGeocodePlace.country}
|
||||
</p>
|
||||
<p>
|
||||
{reverseGeocodePlace.region}:
|
||||
{reverseGeocodePlace.region_visited
|
||||
? $t('adventures.visited')
|
||||
: $t('adventures.not_visited')}
|
||||
<p class="mb-1">
|
||||
<span class="font-semibold">{$t('adventures.region')}:</span>
|
||||
{reverseGeocodePlace.region}
|
||||
{reverseGeocodePlace.region_visited ? '✅' : '❌'}
|
||||
</p>
|
||||
{#if reverseGeocodePlace.city}
|
||||
<p>
|
||||
{reverseGeocodePlace.city}:
|
||||
{reverseGeocodePlace.city_visited
|
||||
? $t('adventures.visited')
|
||||
: $t('adventures.not_visited')}
|
||||
<p class="mb-1">
|
||||
<span class="font-semibold">{$t('adventures.city')}:</span>
|
||||
{reverseGeocodePlace.city}
|
||||
{reverseGeocodePlace.city_visited ? '✅' : '❌'}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{#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')}
|
||||
</button>
|
||||
{/if}
|
||||
{#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
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
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
|
||||
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"
|
||||
></path>
|
||||
</svg>
|
||||
<span
|
||||
>{reverseGeocodePlace.city
|
||||
<span>
|
||||
{reverseGeocodePlace.city
|
||||
? reverseGeocodePlace.city + ', '
|
||||
: ''}{reverseGeocodePlace.region},
|
||||
{reverseGeocodePlace.country}
|
||||
{$t('adventures.will_be_marked')}</span
|
||||
>
|
||||
: ''}{reverseGeocodePlace.region}, {reverseGeocodePlace.country}
|
||||
{$t('adventures.will_be_marked')}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
|
176
frontend/src/lib/components/LodgingCard.svelte
Normal file
176
frontend/src/lib/components/LodgingCard.svelte
Normal 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>
|
|
@ -3,27 +3,19 @@
|
|||
import { addToast } from '$lib/toasts';
|
||||
import { t } from 'svelte-i18n';
|
||||
import MarkdownEditor from './MarkdownEditor.svelte';
|
||||
import { appVersion } from '$lib/config';
|
||||
import { DefaultMarker, MapEvents, MapLibre } from 'svelte-maplibre';
|
||||
import type { Collection, Hotel, ReverseGeocode, OpenStreetMapPlace, Point } from '$lib/types';
|
||||
import type { Collection, Lodging } from '$lib/types';
|
||||
import LocationDropdown from './LocationDropdown.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let collection: Collection;
|
||||
export let hotelToEdit: Hotel | null = null;
|
||||
export let lodgingToEdit: Lodging | null = null;
|
||||
|
||||
let modal: HTMLDialogElement;
|
||||
let constrainDates: boolean = false;
|
||||
let hotel: Hotel = { ...initializeHotel(hotelToEdit) };
|
||||
let lodging: Lodging = { ...initializeLodging(lodgingToEdit) };
|
||||
let fullStartDate: 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
|
||||
function toLocalDatetime(value: string | null): string {
|
||||
|
@ -32,12 +24,32 @@
|
|||
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
|
||||
function initializeHotel(hotelToEdit: Hotel | null): Hotel {
|
||||
function initializeLodging(hotelToEdit: Lodging | null): Lodging {
|
||||
return {
|
||||
id: hotelToEdit?.id || '',
|
||||
user_id: hotelToEdit?.user_id || '',
|
||||
name: hotelToEdit?.name || '',
|
||||
type: hotelToEdit?.type || 'other',
|
||||
description: hotelToEdit?.description || '',
|
||||
rating: hotelToEdit?.rating || NaN,
|
||||
link: hotelToEdit?.link || '',
|
||||
|
@ -49,7 +61,7 @@
|
|||
longitude: hotelToEdit?.longitude || null,
|
||||
location: hotelToEdit?.location || '',
|
||||
is_public: hotelToEdit?.is_public || false,
|
||||
collection: hotelToEdit?.collection || '',
|
||||
collection: hotelToEdit?.collection || collection.id,
|
||||
created_at: hotelToEdit?.created_at || '',
|
||||
updated_at: hotelToEdit?.updated_at || ''
|
||||
};
|
||||
|
@ -63,8 +75,8 @@
|
|||
|
||||
// Handle rating change
|
||||
$: {
|
||||
if (!hotel.rating) {
|
||||
hotel.rating = NaN;
|
||||
if (!lodging.rating) {
|
||||
lodging.rating = NaN;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,35 +100,37 @@
|
|||
async function handleSubmit(event: Event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (hotel.check_in && !hotel.check_out) {
|
||||
const checkInDate = new Date(hotel.check_in);
|
||||
if (lodging.check_in && !lodging.check_out) {
|
||||
const checkInDate = new Date(lodging.check_in);
|
||||
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'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Create or update hotel
|
||||
const url = hotel.id === '' ? '/api/hotels' : `/api/hotels/${hotel.id}`;
|
||||
const method = hotel.id === '' ? 'POST' : 'PATCH';
|
||||
const url = lodging.id === '' ? '/api/lodging' : `/api/lodging/${lodging.id}`;
|
||||
const method = lodging.id === '' ? 'POST' : 'PATCH';
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(hotel)
|
||||
body: JSON.stringify(lodging)
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.id) {
|
||||
hotel = data as Hotel;
|
||||
lodging = data as Lodging;
|
||||
const toastMessage =
|
||||
hotel.id === '' ? 'adventures.adventure_created' : 'adventures.adventure_updated';
|
||||
lodging.id === '' ? 'adventures.adventure_created' : 'adventures.adventure_updated';
|
||||
addToast('success', $t(toastMessage));
|
||||
dispatch('save', hotel);
|
||||
dispatch('save', lodging);
|
||||
} else {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
@ -127,9 +141,7 @@
|
|||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
<div class="modal-box w-11/12 max-w-3xl" role="dialog" on:keydown={handleKeydown} tabindex="0">
|
||||
<h3 class="font-bold text-2xl">
|
||||
{hotelToEdit
|
||||
? $t('transportation.edit_transportation')
|
||||
: $t('transportation.new_transportation')}
|
||||
{lodgingToEdit ? $t('lodging.edit_lodging') : $t('lodging.new_lodging')}
|
||||
</h3>
|
||||
<div class="modal-action items-center">
|
||||
<form method="post" style="width: 100%;" on:submit={handleSubmit}>
|
||||
|
@ -149,7 +161,7 @@
|
|||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
bind:value={hotel.name}
|
||||
bind:value={lodging.name}
|
||||
class="input input-bordered w-full"
|
||||
required
|
||||
/>
|
||||
|
@ -157,7 +169,7 @@
|
|||
<!-- Description -->
|
||||
<div>
|
||||
<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>
|
||||
<!-- Rating -->
|
||||
<div>
|
||||
|
@ -167,7 +179,7 @@
|
|||
min="0"
|
||||
max="5"
|
||||
hidden
|
||||
bind:value={hotel.rating}
|
||||
bind:value={lodging.rating}
|
||||
id="rating"
|
||||
name="rating"
|
||||
class="input input-bordered w-full max-w-xs mt-1"
|
||||
|
@ -177,48 +189,48 @@
|
|||
type="radio"
|
||||
name="rating-2"
|
||||
class="rating-hidden"
|
||||
checked={Number.isNaN(hotel.rating)}
|
||||
checked={Number.isNaN(lodging.rating)}
|
||||
/>
|
||||
<input
|
||||
type="radio"
|
||||
name="rating-2"
|
||||
class="mask mask-star-2 bg-orange-400"
|
||||
on:click={() => (hotel.rating = 1)}
|
||||
checked={hotel.rating === 1}
|
||||
on:click={() => (lodging.rating = 1)}
|
||||
checked={lodging.rating === 1}
|
||||
/>
|
||||
<input
|
||||
type="radio"
|
||||
name="rating-2"
|
||||
class="mask mask-star-2 bg-orange-400"
|
||||
on:click={() => (hotel.rating = 2)}
|
||||
checked={hotel.rating === 2}
|
||||
on:click={() => (lodging.rating = 2)}
|
||||
checked={lodging.rating === 2}
|
||||
/>
|
||||
<input
|
||||
type="radio"
|
||||
name="rating-2"
|
||||
class="mask mask-star-2 bg-orange-400"
|
||||
on:click={() => (hotel.rating = 3)}
|
||||
checked={hotel.rating === 3}
|
||||
on:click={() => (lodging.rating = 3)}
|
||||
checked={lodging.rating === 3}
|
||||
/>
|
||||
<input
|
||||
type="radio"
|
||||
name="rating-2"
|
||||
class="mask mask-star-2 bg-orange-400"
|
||||
on:click={() => (hotel.rating = 4)}
|
||||
checked={hotel.rating === 4}
|
||||
on:click={() => (lodging.rating = 4)}
|
||||
checked={lodging.rating === 4}
|
||||
/>
|
||||
<input
|
||||
type="radio"
|
||||
name="rating-2"
|
||||
class="mask mask-star-2 bg-orange-400"
|
||||
on:click={() => (hotel.rating = 5)}
|
||||
checked={hotel.rating === 5}
|
||||
on:click={() => (lodging.rating = 5)}
|
||||
checked={lodging.rating === 5}
|
||||
/>
|
||||
{#if hotel.rating}
|
||||
{#if lodging.rating}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-error ml-2"
|
||||
on:click={() => (hotel.rating = NaN)}
|
||||
on:click={() => (lodging.rating = NaN)}
|
||||
>
|
||||
{$t('adventures.remove')}
|
||||
</button>
|
||||
|
@ -232,7 +244,7 @@
|
|||
type="url"
|
||||
id="link"
|
||||
name="link"
|
||||
bind:value={hotel.link}
|
||||
bind:value={lodging.link}
|
||||
class="input input-bordered w-full"
|
||||
/>
|
||||
</div>
|
||||
|
@ -247,7 +259,7 @@
|
|||
<!-- Start Date -->
|
||||
<div>
|
||||
<label for="date">
|
||||
{$t('adventures.start_date')}
|
||||
{$t('lodging.check_in')}
|
||||
</label>
|
||||
|
||||
{#if collection && collection.start_date && collection.end_date}<label
|
||||
|
@ -268,7 +280,7 @@
|
|||
type="datetime-local"
|
||||
id="date"
|
||||
name="date"
|
||||
bind:value={hotel.check_in}
|
||||
bind:value={lodging.check_in}
|
||||
min={constrainDates ? fullStartDate : ''}
|
||||
max={constrainDates ? fullEndDate : ''}
|
||||
class="input input-bordered w-full max-w-xs mt-1"
|
||||
|
@ -276,19 +288,19 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- End Date -->
|
||||
{#if hotel.check_in}
|
||||
{#if lodging.check_out}
|
||||
<div>
|
||||
<label for="end_date">
|
||||
{$t('adventures.end_date')}
|
||||
{$t('lodging.check_out')}
|
||||
</label>
|
||||
<div>
|
||||
<input
|
||||
type="datetime-local"
|
||||
id="end_date"
|
||||
name="end_date"
|
||||
min={constrainDates ? hotel.check_in : ''}
|
||||
min={constrainDates ? lodging.check_in : ''}
|
||||
max={constrainDates ? fullEndDate : ''}
|
||||
bind:value={hotel.check_out}
|
||||
bind:value={lodging.check_out}
|
||||
class="input input-bordered w-full max-w-xs mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
@ -298,7 +310,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Location Information -->
|
||||
<LocationDropdown bind:item={hotel} />
|
||||
<LocationDropdown bind:item={lodging} />
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="mt-4">
|
|
@ -113,7 +113,7 @@ export type Collection = {
|
|||
end_date: string | null;
|
||||
transportations?: Transportation[];
|
||||
notes?: Note[];
|
||||
hotels?: Hotel[];
|
||||
lodging?: Lodging[];
|
||||
checklists?: Checklist[];
|
||||
is_archived?: boolean;
|
||||
shared_with: string[] | undefined;
|
||||
|
@ -264,10 +264,11 @@ export type Attachment = {
|
|||
name: string;
|
||||
};
|
||||
|
||||
export type Hotel = {
|
||||
export type Lodging = {
|
||||
id: string;
|
||||
user_id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
description: string | null;
|
||||
rating: number | null;
|
||||
link: string | null;
|
||||
|
|
|
@ -235,7 +235,12 @@
|
|||
"primary": "Primär",
|
||||
"upload": "Hochladen",
|
||||
"view_attachment": "Anhang anzeigen",
|
||||
"of": "von"
|
||||
"of": "von",
|
||||
"city": "Stadt",
|
||||
"display_name": "Anzeigename",
|
||||
"location_details": "Standortdetails",
|
||||
"lodging": "Unterkunft",
|
||||
"region": "Region"
|
||||
},
|
||||
"home": {
|
||||
"desc_1": "Entdecken, planen und erkunden Sie mit Leichtigkeit",
|
||||
|
|
|
@ -191,7 +191,7 @@
|
|||
"no_description_found": "No description found",
|
||||
"adventure_created": "Adventure created",
|
||||
"adventure_create_error": "Failed to create adventure",
|
||||
"hotel": "Hotel",
|
||||
"lodging": "Lodging",
|
||||
"create_adventure": "Create Adventure",
|
||||
"adventure_updated": "Adventure updated",
|
||||
"adventure_update_error": "Failed to update adventure",
|
||||
|
@ -200,6 +200,7 @@
|
|||
"new_adventure": "New Adventure",
|
||||
"basic_information": "Basic Information",
|
||||
"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!",
|
||||
"no_adventures_found": "No adventures found",
|
||||
"mark_region_as_visited": "Mark region {region}, {country} as visited?",
|
||||
|
@ -250,6 +251,9 @@
|
|||
"out_of_range": "Not in itinerary date range",
|
||||
"preview": "Preview",
|
||||
"finding_recommendations": "Discovering hidden gems for your next adventure",
|
||||
"location_details": "Location Details",
|
||||
"city": "City",
|
||||
"region": "Region",
|
||||
"md_instructions": "Write your markdown here...",
|
||||
"days": "days",
|
||||
"attachment_upload_success": "Attachment uploaded successfully!",
|
||||
|
@ -489,6 +493,30 @@
|
|||
"start": "Start",
|
||||
"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": {
|
||||
"adventurelog_results": "AdventureLog Results",
|
||||
"public_adventures": "Public Adventures",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<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 type { PageData } from './$types';
|
||||
import { marked } from 'marked'; // Import the markdown parser
|
||||
|
@ -35,7 +35,8 @@
|
|||
import TransportationModal from '$lib/components/TransportationModal.svelte';
|
||||
import CardCarousel from '$lib/components/CardCarousel.svelte';
|
||||
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;
|
||||
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
|
||||
options = { ...options, events: dates };
|
||||
}
|
||||
|
@ -116,7 +130,7 @@
|
|||
let numAdventures: number = 0;
|
||||
|
||||
let transportations: Transportation[] = [];
|
||||
let hotels: Hotel[] = [];
|
||||
let lodging: Lodging[] = [];
|
||||
let notes: Note[] = [];
|
||||
let checklists: Checklist[] = [];
|
||||
|
||||
|
@ -176,8 +190,8 @@
|
|||
if (collection.transportations) {
|
||||
transportations = collection.transportations;
|
||||
}
|
||||
if (collection.hotels) {
|
||||
hotels = collection.hotels;
|
||||
if (collection.lodging) {
|
||||
lodging = collection.lodging;
|
||||
}
|
||||
if (collection.notes) {
|
||||
notes = collection.notes;
|
||||
|
@ -248,8 +262,8 @@
|
|||
|
||||
let adventureToEdit: Adventure | null = null;
|
||||
let transportationToEdit: Transportation | null = null;
|
||||
let isShowingHotelModal: boolean = false;
|
||||
let hotelToEdit: Hotel | null = null;
|
||||
let isShowingLodgingModal: boolean = false;
|
||||
let lodgingToEdit: Lodging | null = null;
|
||||
let isAdventureModalOpen: boolean = false;
|
||||
let isNoteModalOpen: boolean = false;
|
||||
let noteToEdit: Note | null;
|
||||
|
@ -267,9 +281,9 @@
|
|||
isShowingTransportationModal = true;
|
||||
}
|
||||
|
||||
function editHotel(event: CustomEvent<Hotel>) {
|
||||
hotelToEdit = event.detail;
|
||||
isShowingHotelModal = true;
|
||||
function editLodging(event: CustomEvent<Lodging>) {
|
||||
lodgingToEdit = event.detail;
|
||||
isShowingLodgingModal = true;
|
||||
}
|
||||
|
||||
function saveOrCreateAdventure(event: CustomEvent<Adventure>) {
|
||||
|
@ -368,20 +382,20 @@
|
|||
isShowingTransportationModal = false;
|
||||
}
|
||||
|
||||
function saveOrCreateHotel(event: CustomEvent<Hotel>) {
|
||||
if (hotels.find((hotel) => hotel.id === event.detail.id)) {
|
||||
function saveOrCreateLodging(event: CustomEvent<Lodging>) {
|
||||
if (lodging.find((lodging) => lodging.id === event.detail.id)) {
|
||||
// Update existing hotel
|
||||
hotels = hotels.map((hotel) => {
|
||||
if (hotel.id === event.detail.id) {
|
||||
lodging = lodging.map((lodging) => {
|
||||
if (lodging.id === event.detail.id) {
|
||||
return event.detail;
|
||||
}
|
||||
return hotel;
|
||||
return lodging;
|
||||
});
|
||||
} else {
|
||||
// Create new hotel
|
||||
hotels = [event.detail, ...hotels];
|
||||
// Create new lodging
|
||||
lodging = [event.detail, ...lodging];
|
||||
}
|
||||
isShowingHotelModal = false;
|
||||
isShowingLodgingModal = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -404,11 +418,11 @@
|
|||
/>
|
||||
{/if}
|
||||
|
||||
{#if isShowingHotelModal}
|
||||
<HotelModal
|
||||
{hotelToEdit}
|
||||
on:close={() => (isShowingHotelModal = false)}
|
||||
on:save={saveOrCreateHotel}
|
||||
{#if isShowingLodgingModal}
|
||||
<LodgingModal
|
||||
{lodgingToEdit}
|
||||
on:close={() => (isShowingLodgingModal = false)}
|
||||
on:save={saveOrCreateLodging}
|
||||
{collection}
|
||||
/>
|
||||
{/if}
|
||||
|
@ -541,12 +555,12 @@
|
|||
<button
|
||||
class="btn btn-primary"
|
||||
on:click={() => {
|
||||
isShowingHotelModal = true;
|
||||
isShowingLodgingModal = true;
|
||||
newType = '';
|
||||
hotelToEdit = null;
|
||||
lodgingToEdit = null;
|
||||
}}
|
||||
>
|
||||
{$t('adventures.hotel')}</button
|
||||
{$t('adventures.lodging')}</button
|
||||
>
|
||||
|
||||
<!-- <button
|
||||
|
@ -589,7 +603,7 @@
|
|||
</div>
|
||||
{/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} />
|
||||
{/if}
|
||||
|
||||
|
@ -701,6 +715,63 @@
|
|||
</div>
|
||||
{/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}
|
||||
<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">
|
||||
|
@ -742,7 +813,7 @@
|
|||
{/if}
|
||||
|
||||
<!-- 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} />
|
||||
{/if}
|
||||
{/if}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue