1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-07-25 15:59:38 +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

@ -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.city + ', '
: ''}{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
? reverseGeocodePlace.city + ', '
: ''}{reverseGeocodePlace.region},
{reverseGeocodePlace.country}
{$t('adventures.will_be_marked')}</span
>
<span>
{reverseGeocodePlace.city
? reverseGeocodePlace.city + ', '
: ''}{reverseGeocodePlace.region}, {reverseGeocodePlace.country}
{$t('adventures.will_be_marked')}
</span>
</div>
{/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 { 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">