mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-07-24 07:19:36 +02:00
refactor: enhance UI components with improved styling and layout
- Updated CollectionCard, CountryCard, LodgingCard, NoteCard, RegionCard, TransportationCard, UserCard, and ShareModal components for better visual consistency and responsiveness. - Introduced hover effects and transitions for a more interactive experience. - Improved accessibility by ensuring proper alt text for images and using semantic HTML elements. - Refactored date formatting logic into a utility function for reuse across components. - Added new translations for profile viewing and joined date in the localization files.
This commit is contained in:
parent
3acfc9f228
commit
81006af027
22 changed files with 534 additions and 364 deletions
|
@ -321,11 +321,8 @@
|
||||||
.line-clamp-2 {
|
.line-clamp-2 {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
|
line-clamp: 2;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shadow-3xl {
|
|
||||||
box-shadow: 0 35px 60px -12px rgba(0, 0, 0, 0.25);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -62,43 +62,53 @@
|
||||||
on:confirm={deleteChecklist}
|
on:confirm={deleteChecklist}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div
|
<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 overflow-hidden"
|
class="card w-full max-w-md bg-base-300 text-base-content shadow-2xl hover:shadow-3xl transition-all duration-300 border border-base-300 hover:border-primary/20 group"
|
||||||
>
|
>
|
||||||
<div class="card-body">
|
<div class="card-body p-6 space-y-4">
|
||||||
<div class="flex justify-between">
|
<!-- Header -->
|
||||||
<h2 class="text-2xl font-semibold -mt-2 break-words text-wrap">
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
|
||||||
{checklist.name}
|
<h2 class="text-xl font-bold break-words">{checklist.name}</h2>
|
||||||
</h2>
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<div class="badge badge-primary">{$t('adventures.checklist')}</div>
|
||||||
|
{#if unlinked}
|
||||||
|
<div class="badge badge-error">{$t('adventures.out_of_range')}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="badge badge-primary">{$t('adventures.checklist')}</div>
|
|
||||||
|
<!-- Checklist Stats -->
|
||||||
{#if checklist.items.length > 0}
|
{#if checklist.items.length > 0}
|
||||||
<p>
|
<p class="text-sm">
|
||||||
{checklist.items.length}
|
{checklist.items.length}
|
||||||
{checklist.items.length > 1 ? $t('checklist.items') : $t('checklist.item')}
|
{checklist.items.length > 1 ? $t('checklist.items') : $t('checklist.item')}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#if unlinked}
|
|
||||||
<div class="badge badge-error">{$t('adventures.out_of_range')}</div>
|
<!-- Date -->
|
||||||
{/if}
|
|
||||||
{#if checklist.date && checklist.date !== ''}
|
{#if checklist.date && checklist.date !== ''}
|
||||||
<div class="inline-flex items-center">
|
<div class="inline-flex items-center gap-2 text-sm">
|
||||||
<Calendar class="w-5 h-5 mr-1" />
|
<Calendar class="w-5 h-5 text-primary" />
|
||||||
<p>{new Date(checklist.date).toLocaleDateString(undefined, { timeZone: 'UTC' })}</p>
|
<p>{new Date(checklist.date).toLocaleDateString(undefined, { timeZone: 'UTC' })}</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="card-actions justify-end">
|
|
||||||
<button class="btn btn-neutral-200 mb-2" on:click={editChecklist}>
|
<!-- Actions -->
|
||||||
<Launch class="w-6 h-6" />{$t('notes.open')}
|
<div class="pt-4 border-t border-base-300 flex justify-end gap-2">
|
||||||
|
<button class="btn btn-neutral btn-sm flex items-center gap-1" on:click={editChecklist}>
|
||||||
|
<Launch class="w-5 h-5" />
|
||||||
|
{$t('notes.open')}
|
||||||
</button>
|
</button>
|
||||||
{#if checklist.user_id == user?.uuid || (collection && user && collection.shared_with && collection.shared_with.includes(user.uuid))}
|
{#if checklist.user_id == user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))}
|
||||||
<button
|
<button
|
||||||
id="delete_adventure"
|
id="delete_adventure"
|
||||||
data-umami-event="Delete Checklist"
|
data-umami-event="Delete Checklist"
|
||||||
class="btn btn-warning"
|
class="btn btn-secondary btn-sm flex items-center gap-1"
|
||||||
on:click={() => (isWarningModalOpen = true)}><TrashCan class="w-6 h-6" /></button
|
on:click={() => (isWarningModalOpen = true)}
|
||||||
>
|
>
|
||||||
|
<TrashCan class="w-5 h-5" />
|
||||||
|
{$t('adventures.delete')}
|
||||||
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -41,25 +41,40 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<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 overflow-hidden"
|
class="card w-full max-w-md bg-base-300 text-base-content shadow-2xl hover:shadow-3xl transition-all duration-300 border border-base-300 hover:border-primary/20 group overflow-hidden"
|
||||||
>
|
>
|
||||||
<div class="card-body">
|
<div class="card-body p-6 space-y-4">
|
||||||
<h2 class="card-title overflow-ellipsis">{city.name}</h2>
|
<!-- Header -->
|
||||||
|
<h2 class="text-xl font-bold truncate">{city.name}</h2>
|
||||||
|
|
||||||
|
<!-- Metadata Badges -->
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<div class="badge badge-primary">
|
<div class="badge badge-primary">
|
||||||
{city.region_name}, {city.country_name}
|
{city.region_name}, {city.country_name}
|
||||||
</div>
|
</div>
|
||||||
<div class="badge badge-neutral-300">{city.region}</div>
|
<div class="badge badge-neutral-300">Region ID: {city.region}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions justify-end">
|
|
||||||
{#if !visited}
|
<!-- Actions -->
|
||||||
<button class="btn btn-primary" on:click={markVisited}
|
<div class="pt-4 border-t border-base-300 flex justify-end gap-2">
|
||||||
>{$t('adventures.mark_visited')}</button
|
{#if visited === false}
|
||||||
>
|
<button class="btn btn-primary btn-sm" on:click={markVisited}>
|
||||||
|
{$t('adventures.mark_visited')}
|
||||||
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if visited}
|
{#if visited === true}
|
||||||
<button class="btn btn-warning" on:click={removeVisit}>{$t('adventures.remove')}</button>
|
<button class="btn btn-warning btn-sm" on:click={removeVisit}>
|
||||||
|
{$t('adventures.remove')}
|
||||||
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.truncate {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -84,99 +84,156 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div
|
<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"
|
class="card w-full max-w-md bg-base-300 shadow-2xl hover:shadow-3xl transition-all duration-300 border border-base-300 hover:border-primary/20 group"
|
||||||
>
|
>
|
||||||
<CardCarousel {adventures} />
|
<!-- Image Carousel -->
|
||||||
<div class="card-body">
|
<div class="relative overflow-hidden rounded-t-2xl">
|
||||||
<div class="flex justify-between">
|
<CardCarousel {adventures} />
|
||||||
<button
|
|
||||||
on:click={() => goto(`/collections/${collection.id}`)}
|
<!-- Badge Overlay -->
|
||||||
class="text-2xl font-semibold -mt-2 break-words text-wrap hover:underline"
|
<div class="absolute top-4 left-4 flex flex-col gap-2">
|
||||||
>
|
<div class="badge badge-sm badge-secondary shadow-lg">
|
||||||
{collection.name}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="inline-flex gap-2 mb-2">
|
|
||||||
<div class="badge badge-secondary">
|
|
||||||
{collection.is_public ? $t('adventures.public') : $t('adventures.private')}
|
{collection.is_public ? $t('adventures.public') : $t('adventures.private')}
|
||||||
</div>
|
</div>
|
||||||
{#if collection.is_archived}
|
{#if collection.is_archived}
|
||||||
<div class="badge badge-warning">{$t('adventures.archived')}</div>
|
<div class="badge badge-sm badge-warning shadow-lg">
|
||||||
|
{$t('adventures.archived')}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<p>{collection.adventures.length} {$t('navbar.adventures')}</p>
|
</div>
|
||||||
{#if collection.start_date && collection.end_date}
|
|
||||||
<p>
|
|
||||||
{$t('adventures.dates')}: {new Date(collection.start_date).toLocaleDateString(undefined, {
|
|
||||||
timeZone: 'UTC'
|
|
||||||
})} -
|
|
||||||
{new Date(collection.end_date).toLocaleDateString(undefined, { timeZone: 'UTC' })}
|
|
||||||
</p>
|
|
||||||
<!-- display the duration in days -->
|
|
||||||
<p>
|
|
||||||
{$t('adventures.duration')}: {Math.floor(
|
|
||||||
(new Date(collection.end_date).getTime() - new Date(collection.start_date).getTime()) /
|
|
||||||
(1000 * 60 * 60 * 24)
|
|
||||||
) + 1}{' '}
|
|
||||||
days
|
|
||||||
</p>{/if}
|
|
||||||
|
|
||||||
<div class="card-actions justify-end">
|
<!-- Content -->
|
||||||
|
<div class="card-body p-6 space-y-4">
|
||||||
|
<!-- Title -->
|
||||||
|
<div class="space-y-3">
|
||||||
|
<button
|
||||||
|
on:click={() => goto(`/collections/${collection.id}`)}
|
||||||
|
class="text-xl font-bold text-left hover:text-primary transition-colors duration-200 line-clamp-2 group-hover:underline"
|
||||||
|
>
|
||||||
|
{collection.name}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Adventure Count -->
|
||||||
|
<p class="text-sm text-base-content/70">
|
||||||
|
{collection.adventures.length}
|
||||||
|
{$t('navbar.adventures')}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Date Range -->
|
||||||
|
{#if collection.start_date && collection.end_date}
|
||||||
|
<p class="text-sm font-medium">
|
||||||
|
{$t('adventures.dates')}:
|
||||||
|
{new Date(collection.start_date).toLocaleDateString(undefined, { timeZone: 'UTC' })} –
|
||||||
|
{new Date(collection.end_date).toLocaleDateString(undefined, { timeZone: 'UTC' })}
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-base-content/60">
|
||||||
|
{$t('adventures.duration')}: {Math.floor(
|
||||||
|
(new Date(collection.end_date).getTime() - new Date(collection.start_date).getTime()) /
|
||||||
|
(1000 * 60 * 60 * 24)
|
||||||
|
) + 1} days
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="pt-4 border-t border-base-300">
|
||||||
{#if type == 'link'}
|
{#if type == 'link'}
|
||||||
<button class="btn btn-primary" on:click={() => dispatch('link', collection.id)}>
|
<button class="btn btn-primary btn-block" on:click={() => dispatch('link', collection.id)}>
|
||||||
<Plus class="w-5 h-5 mr-1" />
|
<Plus class="w-4 h-4" />
|
||||||
|
{$t('adventures.add_to_collection')}
|
||||||
</button>
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="dropdown dropdown-end">
|
<div class="flex justify-between items-center">
|
||||||
<div tabindex="0" role="button" class="btn btn-neutral-200">
|
<button
|
||||||
<DotsHorizontal class="w-6 h-6" />
|
class="btn btn-neutral btn-sm flex-1 mr-2"
|
||||||
</div>
|
on:click={() => goto(`/collections/${collection.id}`)}
|
||||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
|
||||||
<ul
|
|
||||||
tabindex="0"
|
|
||||||
class="dropdown-content menu bg-base-100 rounded-box z-[1] w-64 p-2 shadow"
|
|
||||||
>
|
>
|
||||||
{#if type != 'link' && type != 'viewonly'}
|
<Launch class="w-4 h-4" />
|
||||||
<button
|
{$t('adventures.open_details')}
|
||||||
class="btn btn-neutral mb-2"
|
</button>
|
||||||
on:click={() => goto(`/collections/${collection.id}`)}
|
<div class="dropdown dropdown-end">
|
||||||
><Launch class="w-5 h-5 mr-1" />{$t('adventures.open_details')}</button
|
<button type="button" class="btn btn-square btn-sm btn-base-300">
|
||||||
>
|
<DotsHorizontal class="w-5 h-5" />
|
||||||
{#if !collection.is_archived}
|
</button>
|
||||||
<button class="btn btn-neutral mb-2" on:click={editAdventure}>
|
<ul
|
||||||
<FileDocumentEdit class="w-6 h-6" />{$t('adventures.edit_collection')}
|
class="dropdown-content menu bg-base-100 rounded-box z-[1] w-64 p-2 shadow-xl border border-base-300"
|
||||||
</button>
|
>
|
||||||
<button class="btn btn-neutral mb-2" on:click={() => (isShareModalOpen = true)}>
|
{#if type != 'viewonly'}
|
||||||
<ShareVariant class="w-6 h-6" />{$t('adventures.share')}
|
<li>
|
||||||
</button>
|
<button class="flex items-center gap-2" on:click={editAdventure}>
|
||||||
|
<FileDocumentEdit class="w-4 h-4" />
|
||||||
|
{$t('adventures.edit_collection')}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
class="flex items-center gap-2"
|
||||||
|
on:click={() => (isShareModalOpen = true)}
|
||||||
|
>
|
||||||
|
<ShareVariant class="w-4 h-4" />
|
||||||
|
{$t('adventures.share')}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{#if collection.is_archived}
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
class="flex items-center gap-2"
|
||||||
|
on:click={() => archiveCollection(false)}
|
||||||
|
>
|
||||||
|
<ArchiveArrowUp class="w-4 h-4" />
|
||||||
|
{$t('adventures.unarchive')}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{:else}
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
class="flex items-center gap-2"
|
||||||
|
on:click={() => archiveCollection(true)}
|
||||||
|
>
|
||||||
|
<ArchiveArrowDown class="w-4 h-4" />
|
||||||
|
{$t('adventures.archive')}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
<div class="divider my-1"></div>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
id="delete_collection"
|
||||||
|
data-umami-event="Delete Collection"
|
||||||
|
class="text-error flex items-center gap-2"
|
||||||
|
on:click={() => (isWarningModalOpen = true)}
|
||||||
|
>
|
||||||
|
<TrashCan class="w-4 h-4" />
|
||||||
|
{$t('adventures.delete')}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
{/if}
|
{/if}
|
||||||
{#if collection.is_archived}
|
{#if type == 'viewonly'}
|
||||||
<button class="btn btn-neutral mb-2" on:click={() => archiveCollection(false)}>
|
<li>
|
||||||
<ArchiveArrowUp class="w-6 h-6 mr-1" />{$t('adventures.unarchive')}
|
<button
|
||||||
</button>
|
class="flex items-center gap-2"
|
||||||
{:else}
|
on:click={() => goto(`/collections/${collection.id}`)}
|
||||||
<button class="btn btn-neutral mb-2" on:click={() => archiveCollection(true)}>
|
>
|
||||||
<ArchiveArrowDown class="w-6 h-6 mr" />{$t('adventures.archive')}
|
<Launch class="w-4 h-4" />
|
||||||
</button>
|
{$t('adventures.open_details')}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
{/if}
|
{/if}
|
||||||
<button
|
</ul>
|
||||||
id="delete_adventure"
|
</div>
|
||||||
data-umami-event="Delete Adventure"
|
|
||||||
class="btn btn-warning"
|
|
||||||
on:click={() => (isWarningModalOpen = true)}
|
|
||||||
><TrashCan class="w-6 h-6" />{$t('adventures.delete')}</button
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
{#if type == 'viewonly'}
|
|
||||||
<button
|
|
||||||
class="btn btn-neutral mb-2"
|
|
||||||
on:click={() => goto(`/collections/${collection.id}`)}
|
|
||||||
><Launch class="w-5 h-5 mr-1" />{$t('adventures.open_details')}</button
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.line-clamp-2 {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
import MapMarkerStar from '~icons/mdi/map-marker-star';
|
import MapMarkerStar from '~icons/mdi/map-marker-star';
|
||||||
|
import Launch from '~icons/mdi/launch';
|
||||||
|
|
||||||
export let country: Country;
|
export let country: Country;
|
||||||
|
|
||||||
|
@ -14,24 +15,31 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<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 overflow-hidden"
|
class="card w-full max-w-md bg-base-300 text-base-content shadow-2xl hover:shadow-3xl transition-all duration-300 border border-base-300 hover:border-primary/20 group overflow-hidden"
|
||||||
>
|
>
|
||||||
|
<!-- Flag Image -->
|
||||||
<figure>
|
<figure>
|
||||||
<!-- svelte-ignore a11y-img-redundant-alt -->
|
<img src={country.flag_url} alt={`Flag of ${country.name}`} class="w-full h-48 object-cover" />
|
||||||
<img src={country.flag_url} alt="No image available" class="w-full h-48 object-cover" />
|
|
||||||
</figure>
|
</figure>
|
||||||
<div class="card-body">
|
|
||||||
<h2 class="card-title overflow-ellipsis">{country.name}</h2>
|
<!-- Content -->
|
||||||
|
<div class="card-body p-6 space-y-4">
|
||||||
|
<!-- Title -->
|
||||||
|
<h2 class="text-xl font-bold truncate">{country.name}</h2>
|
||||||
|
|
||||||
|
<!-- Info Badges -->
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
{#if country.subregion}
|
{#if country.subregion}
|
||||||
<div class="badge badge-primary">{country.subregion}</div>
|
<div class="badge badge-primary">{country.subregion}</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if country.capital}
|
{#if country.capital}
|
||||||
<div class="badge badge-secondary">
|
<div class="badge badge-secondary inline-flex items-center gap-1">
|
||||||
<MapMarkerStar class="-ml-1 mr-1" />{country.capital}
|
<MapMarkerStar class="w-4 h-4" />
|
||||||
|
{country.capital}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if country.num_visits > 0 && country.num_visits != country.num_regions}
|
|
||||||
|
{#if country.num_visits > 0 && country.num_visits !== country.num_regions}
|
||||||
<div class="badge badge-accent">
|
<div class="badge badge-accent">
|
||||||
Visited {country.num_visits} Region{country.num_visits > 1 ? 's' : ''}
|
Visited {country.num_visits} Region{country.num_visits > 1 ? 's' : ''}
|
||||||
</div>
|
</div>
|
||||||
|
@ -42,9 +50,20 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-actions justify-end">
|
<!-- Actions -->
|
||||||
<!-- <button class="btn btn-info" on:click={moreInfo}>More Info</button> -->
|
<div class="pt-4 border-t border-base-300 flex justify-end">
|
||||||
<button class="btn btn-primary" on:click={nav}>{$t('notes.open')}</button>
|
<button class="btn btn-primary btn-sm flex items-center gap-1" on:click={nav}>
|
||||||
|
<Launch class="w-4 h-4" />
|
||||||
|
{$t('notes.open')}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.truncate {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import DeleteWarning from './DeleteWarning.svelte';
|
import DeleteWarning from './DeleteWarning.svelte';
|
||||||
import { LODGING_TYPES_ICONS } from '$lib';
|
import { LODGING_TYPES_ICONS } from '$lib';
|
||||||
|
import { formatDateInTimezone } from '$lib/dateUtils';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
@ -17,24 +18,6 @@
|
||||||
return '🏨';
|
return '🏨';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDateInTimezone(utcDate: string, timezone?: string): string {
|
|
||||||
if (!utcDate) return '';
|
|
||||||
try {
|
|
||||||
return new Intl.DateTimeFormat(undefined, {
|
|
||||||
timeZone: timezone,
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
hour12: true
|
|
||||||
}).format(new Date(utcDate));
|
|
||||||
} catch {
|
|
||||||
return new Date(utcDate).toLocaleString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export let lodging: Lodging;
|
export let lodging: Lodging;
|
||||||
export let user: User | null = null;
|
export let user: User | null = null;
|
||||||
export let collection: Collection | null = null;
|
export let collection: Collection | null = null;
|
||||||
|
@ -109,64 +92,71 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div
|
<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"
|
class="card w-full max-w-md bg-base-300 text-base-content shadow-2xl hover:shadow-3xl transition-all duration-300 border border-base-300 hover:border-primary/20 group"
|
||||||
>
|
>
|
||||||
<div class="card-body space-y-4">
|
<div class="card-body p-6 space-y-4">
|
||||||
<!-- Title and Type -->
|
<!-- Header -->
|
||||||
<h2 class="text-2xl font-semibold">{lodging.name}</h2>
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
|
||||||
<div>
|
<h2 class="text-xl font-bold truncate">{lodging.name}</h2>
|
||||||
<div class="badge badge-secondary">
|
<div class="flex flex-wrap gap-2">
|
||||||
{$t(`lodging.${lodging.type}`) + ' ' + getLodgingIcon(lodging.type)}
|
<div class="badge badge-secondary">
|
||||||
|
{$t(`lodging.${lodging.type}`)}
|
||||||
|
{getLodgingIcon(lodging.type)}
|
||||||
|
</div>
|
||||||
|
{#if unlinked}
|
||||||
|
<div class="badge badge-error">{$t('adventures.out_of_range')}</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if unlinked}
|
|
||||||
<div class="badge badge-error">{$t('adventures.out_of_range')}</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Location -->
|
<!-- Location Info -->
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
{#if lodging.location}
|
{#if lodging.location}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="font-medium text-sm">{$t('adventures.location')}:</span>
|
<span class="text-sm font-medium">{$t('adventures.location')}:</span>
|
||||||
<p>{lodging.location}</p>
|
<p class="text-sm break-words">{lodging.location}</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if lodging.check_in && lodging.check_out}
|
{#if lodging.check_in && lodging.check_out}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="font-medium text-sm">{$t('adventures.dates')}:</span>
|
<span class="text-sm font-medium">{$t('adventures.dates')}:</span>
|
||||||
<p>
|
<p class="text-sm">
|
||||||
{formatDateInTimezone(lodging.check_in ?? '', lodging.timezone ?? undefined)} –
|
{formatDateInTimezone(lodging.check_in, lodging.timezone)} –
|
||||||
{formatDateInTimezone(lodging.check_out ?? '', lodging.timezone ?? undefined)}
|
{formatDateInTimezone(lodging.check_out, lodging.timezone)}
|
||||||
{#if lodging.timezone}
|
{#if lodging.timezone}
|
||||||
<span class="text-xs opacity-60 ml-1">({lodging.timezone})</span>
|
<span class="ml-1 text-xs opacity-60">({lodging.timezone})</span>
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if lodging.user_id == user?.uuid || (collection && user && collection.shared_with && collection.shared_with.includes(user.uuid))}
|
</div>
|
||||||
|
|
||||||
|
<!-- Reservation Info -->
|
||||||
|
{#if lodging.user_id == user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))}
|
||||||
|
<div class="space-y-2">
|
||||||
{#if lodging.reservation_number}
|
{#if lodging.reservation_number}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="font-medium text-sm">{$t('adventures.reservation_number')}:</span>
|
<span class="text-sm font-medium">{$t('adventures.reservation_number')}:</span>
|
||||||
<p>{lodging.reservation_number}</p>
|
<p class="text-sm break-all">{lodging.reservation_number}</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if lodging.price}
|
{#if lodging.price}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="font-medium text-sm">{$t('adventures.price')}:</span>
|
<span class="text-sm font-medium">{$t('adventures.price')}:</span>
|
||||||
<p>{lodging.price}</p>
|
<p class="text-sm">{lodging.price}</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
|
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
{#if lodging.user_id == user?.uuid || (collection && user && collection.shared_with && collection.shared_with.includes(user.uuid))}
|
{#if lodging.user_id == user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))}
|
||||||
<div class="card-actions justify-end">
|
<div class="pt-4 border-t border-base-300 flex justify-end gap-2">
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary btn-sm flex items-center gap-1"
|
class="btn btn-neutral btn-sm flex items-center gap-1"
|
||||||
on:click={editTransportation}
|
on:click={editTransportation}
|
||||||
title="Edit"
|
title={$t('transportation.edit')}
|
||||||
>
|
>
|
||||||
<FileDocumentEdit class="w-5 h-5" />
|
<FileDocumentEdit class="w-5 h-5" />
|
||||||
<span>{$t('transportation.edit')}</span>
|
<span>{$t('transportation.edit')}</span>
|
||||||
|
@ -174,7 +164,7 @@
|
||||||
<button
|
<button
|
||||||
on:click={() => (isWarningModalOpen = true)}
|
on:click={() => (isWarningModalOpen = true)}
|
||||||
class="btn btn-secondary btn-sm flex items-center gap-1"
|
class="btn btn-secondary btn-sm flex items-center gap-1"
|
||||||
title="Delete"
|
title={$t('adventures.delete')}
|
||||||
>
|
>
|
||||||
<TrashCanOutline class="w-5 h-5" />
|
<TrashCanOutline class="w-5 h-5" />
|
||||||
<span>{$t('adventures.delete')}</span>
|
<span>{$t('adventures.delete')}</span>
|
||||||
|
|
|
@ -65,63 +65,75 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="card w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-md xl:max-w-md overflow-hidden bg-neutral text-neutral-content shadow-xl"
|
class="card w-full max-w-md bg-base-300 text-base-content shadow-2xl hover:shadow-3xl transition-all duration-300 border border-base-300 hover:border-primary/20 group"
|
||||||
>
|
>
|
||||||
<div class="card-body">
|
<div class="card-body p-6 space-y-4">
|
||||||
<div class="flex justify-between">
|
<!-- Header -->
|
||||||
<h2 class="text-2xl font-semibold -mt-2 break-words text-wrap">
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
|
||||||
{note.name}
|
<h2 class="text-xl font-bold break-words">{note.name}</h2>
|
||||||
</h2>
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<div class="badge badge-primary">{$t('adventures.note')}</div>
|
||||||
|
{#if unlinked}
|
||||||
|
<div class="badge badge-error">{$t('adventures.out_of_range')}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="badge badge-primary">{$t('adventures.note')}</div>
|
|
||||||
{#if unlinked}
|
<!-- Note Content -->
|
||||||
<div class="badge badge-error">{$t('adventures.out_of_range')}</div>
|
{#if note.content && note.content?.length > 0}
|
||||||
{/if}
|
|
||||||
{#if note.content && note.content.length > 0}
|
|
||||||
<article
|
<article
|
||||||
class="prose overflow-auto max-h-72 max-w-full p-4 border border-neutral bg-base-100 rounded-lg mb-4 mt-4"
|
class="prose overflow-auto max-h-72 max-w-full p-4 border border-base-300 bg-base-100 rounded-lg"
|
||||||
>
|
>
|
||||||
{@html renderMarkdown(note.content || '')}
|
{@html renderMarkdown(note.content || '')}
|
||||||
</article>
|
</article>
|
||||||
{/if}
|
{/if}
|
||||||
{#if note.links && note.links.length > 0}
|
|
||||||
<p>
|
<!-- Links -->
|
||||||
{note.links.length}
|
{#if note.links && note.links?.length > 0}
|
||||||
{note.links.length > 1 ? $t('adventures.links') : $t('adventures.link')}
|
<div class="space-y-1">
|
||||||
</p>
|
<p class="text-sm font-medium">
|
||||||
<ul class="list-disc pl-6">
|
{note.links.length}
|
||||||
{#each note.links.slice(0, 3) as link}
|
{note.links.length > 1 ? $t('adventures.links') : $t('adventures.link')}
|
||||||
<li>
|
</p>
|
||||||
<a class="link link-primary" href={link}>
|
<ul class="list-disc pl-5 text-sm">
|
||||||
{link.split('//')[1].split('/', 1)[0]}
|
{#each note.links.slice(0, 3) as link}
|
||||||
</a>
|
<li>
|
||||||
</li>
|
<a class="link link-primary" href={link} target="_blank" rel="noopener noreferrer">
|
||||||
{/each}
|
{link.split('//')[1]?.split('/', 1)[0]}
|
||||||
{#if note.links.length > 3}
|
</a>
|
||||||
<li>…</li>
|
</li>
|
||||||
{/if}
|
{/each}
|
||||||
</ul>
|
{#if note.links.length > 3}
|
||||||
|
<li>…</li>
|
||||||
|
{/if}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<!-- Date -->
|
||||||
{#if note.date && note.date !== ''}
|
{#if note.date && note.date !== ''}
|
||||||
<div class="inline-flex items-center">
|
<div class="inline-flex items-center gap-2 text-sm">
|
||||||
<Calendar class="w-5 h-5 mr-1" />
|
<Calendar class="w-5 h-5 text-primary" />
|
||||||
<p>{new Date(note.date).toLocaleDateString(undefined, { timeZone: 'UTC' })}</p>
|
<p>{new Date(note.date).toLocaleDateString(undefined, { timeZone: 'UTC' })}</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="card-actions justify-end">
|
|
||||||
<!-- <button class="btn btn-neutral mb-2" on:click={() => goto(`/notes/${note.id}`)}
|
<!-- Actions -->
|
||||||
><Launch class="w-6 h-6" />Open Details</button
|
<div class="pt-4 border-t border-base-300 flex justify-end gap-2">
|
||||||
> -->
|
<button class="btn btn-neutral btn-sm flex items-center gap-1" on:click={editNote}>
|
||||||
<button class="btn btn-neutral-200 mb-2" on:click={editNote}>
|
<Launch class="w-5 h-5" />
|
||||||
<Launch class="w-6 h-6" />{$t('notes.open')}
|
{$t('notes.open')}
|
||||||
</button>
|
</button>
|
||||||
{#if note.user_id == user?.uuid || (collection && user && collection.shared_with && collection.shared_with.includes(user.uuid))}
|
{#if note.user_id == user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))}
|
||||||
<button
|
<button
|
||||||
id="delete_adventure"
|
id="delete_adventure"
|
||||||
data-umami-event="Delete Adventure"
|
data-umami-event="Delete Adventure"
|
||||||
class="btn btn-warning"
|
class="btn btn-secondary btn-sm flex items-center gap-1"
|
||||||
on:click={() => (isWarningModalOpen = true)}><TrashCan class="w-6 h-6" /></button
|
on:click={() => (isWarningModalOpen = true)}
|
||||||
>
|
>
|
||||||
|
<TrashCan class="w-5 h-5" />
|
||||||
|
{$t('adventures.delete')}
|
||||||
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -50,36 +50,47 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<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 overflow-hidden"
|
class="card w-full max-w-md bg-base-300 text-base-content shadow-2xl hover:shadow-3xl transition-all duration-300 border border-base-300 hover:border-primary/20 group overflow-hidden"
|
||||||
>
|
>
|
||||||
<div class="card-body">
|
<div class="card-body p-6 space-y-4">
|
||||||
<h2 class="card-title overflow-ellipsis">{region.name}</h2>
|
<!-- Header -->
|
||||||
<div>
|
<h2 class="text-xl font-bold truncate">{region.name}</h2>
|
||||||
<div class="badge badge-primary">
|
|
||||||
<p>{region.country_name}</p>
|
<!-- Metadata Badges -->
|
||||||
</div>
|
<div class="flex flex-wrap gap-2">
|
||||||
<div class="badge badge-neutral-300">
|
<div class="badge badge-primary">{region.country_name}</div>
|
||||||
<p>{region.num_cities} {$t('worldtravel.cities')}</p>
|
<div class="badge badge-neutral">
|
||||||
</div>
|
{region.num_cities}
|
||||||
<div class="badge badge-neutral-300">
|
{$t('worldtravel.cities')}
|
||||||
<p>{region.id}</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="badge badge-neutral-300">ID: {region.id}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions justify-end">
|
|
||||||
<!-- <button class="btn btn-info" on:click={moreInfo}>More Info</button> -->
|
<!-- Actions -->
|
||||||
{#if !visited && visited !== undefined}
|
<div class="pt-4 border-t border-base-300 flex flex-wrap gap-2 justify-end">
|
||||||
<button class="btn btn-primary" on:click={markVisited}
|
{#if visited === false}
|
||||||
>{$t('adventures.mark_visited')}</button
|
<button class="btn btn-primary btn-sm" on:click={markVisited}>
|
||||||
>
|
{$t('adventures.mark_visited')}
|
||||||
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if visited && visited !== undefined}
|
{#if visited === true}
|
||||||
<button class="btn btn-warning" on:click={removeVisit}>{$t('adventures.remove')}</button>
|
<button class="btn btn-warning btn-sm" on:click={removeVisit}>
|
||||||
|
{$t('adventures.remove')}
|
||||||
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if region.num_cities > 0}
|
{#if region.num_cities > 0}
|
||||||
<button class="btn btn-neutral-300" on:click={goToCity}
|
<button class="btn btn-neutral btn-sm" on:click={goToCity}>
|
||||||
>{$t('worldtravel.view_cities')}</button
|
{$t('worldtravel.view_cities')}
|
||||||
>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.truncate {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -89,47 +89,73 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<dialog id="my_modal_1" class="modal">
|
<dialog id="my_modal_1" class="modal">
|
||||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
|
||||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||||
<div class="modal-box w-11/12 max-w-5xl" role="dialog" on:keydown={handleKeydown} tabindex="0">
|
<div
|
||||||
<h3 class="font-bold text-lg">{$t('adventures.share')} {collection.name}</h3>
|
class="modal-box w-11/12 max-w-5xl p-6 space-y-6"
|
||||||
<p class="py-1">{$t('share.share_desc')}</p>
|
role="dialog"
|
||||||
<div class="divider"></div>
|
tabindex="0"
|
||||||
<h3 class="font-bold text-md">{$t('share.shared_with')}</h3>
|
on:keydown={handleKeydown}
|
||||||
<ul>
|
>
|
||||||
{#each sharedWithUsers as user}
|
<!-- Title -->
|
||||||
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
|
<div class="space-y-1">
|
||||||
<UserCard
|
<h3 class="text-2xl font-bold">
|
||||||
{user}
|
{$t('adventures.share')}
|
||||||
shared_with={collection.shared_with}
|
{collection.name}
|
||||||
sharing={true}
|
</h3>
|
||||||
on:share={(event) => share(event.detail)}
|
<p class="text-base-content/70">{$t('share.share_desc')}</p>
|
||||||
on:unshare={(event) => unshare(event.detail)}
|
</div>
|
||||||
/>
|
|
||||||
|
<!-- Shared With Section -->
|
||||||
|
<div>
|
||||||
|
<h4 class="text-lg font-semibold mb-2">{$t('share.shared_with')}</h4>
|
||||||
|
{#if sharedWithUsers.length > 0}
|
||||||
|
<div
|
||||||
|
class="grid gap-4 grid-cols-1 sm:grid-cols-2 md:grid-cols-3 max-h-80 overflow-y-auto pr-2"
|
||||||
|
>
|
||||||
|
{#each sharedWithUsers as user}
|
||||||
|
<UserCard
|
||||||
|
{user}
|
||||||
|
shared_with={collection.shared_with}
|
||||||
|
sharing={true}
|
||||||
|
on:share={(event) => share(event.detail)}
|
||||||
|
on:unshare={(event) => unshare(event.detail)}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{:else}
|
||||||
{#if sharedWithUsers.length === 0}
|
<p class="text-neutral-content italic">{$t('share.no_users_shared')}</p>
|
||||||
<p class="text-neutral-content">{$t('share.no_users_shared')}</p>
|
|
||||||
{/if}
|
{/if}
|
||||||
</ul>
|
</div>
|
||||||
|
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<h3 class="font-bold text-md">{$t('share.not_shared_with')}</h3>
|
|
||||||
<ul>
|
<!-- Not Shared With Section -->
|
||||||
{#each notSharedWithUsers as user}
|
<div>
|
||||||
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
|
<h4 class="text-lg font-semibold mb-2">{$t('share.not_shared_with')}</h4>
|
||||||
<UserCard
|
{#if notSharedWithUsers.length > 0}
|
||||||
{user}
|
<div
|
||||||
shared_with={collection.shared_with}
|
class="grid gap-4 grid-cols-1 sm:grid-cols-2 md:grid-cols-3 max-h-80 overflow-y-auto pr-2"
|
||||||
sharing={true}
|
>
|
||||||
on:share={(event) => share(event.detail)}
|
{#each notSharedWithUsers as user}
|
||||||
on:unshare={(event) => unshare(event.detail)}
|
<UserCard
|
||||||
/>
|
{user}
|
||||||
|
shared_with={collection.shared_with}
|
||||||
|
sharing={true}
|
||||||
|
on:share={(event) => share(event.detail)}
|
||||||
|
on:unshare={(event) => unshare(event.detail)}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{:else}
|
||||||
{#if notSharedWithUsers.length === 0}
|
<p class="text-neutral-content italic">{$t('share.no_users_shared')}</p>
|
||||||
<p class="text-neutral-content">{$t('share.no_users_shared')}</p>
|
|
||||||
{/if}
|
{/if}
|
||||||
</ul>
|
</div>
|
||||||
<button class="btn btn-primary mt-4" on:click={close}>{$t('about.close')}</button>
|
|
||||||
|
<!-- Action -->
|
||||||
|
<div class="pt-4 border-t border-base-300 flex justify-end">
|
||||||
|
<button class="btn btn-primary" on:click={close}>
|
||||||
|
{$t('about.close')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
import DeleteWarning from './DeleteWarning.svelte';
|
import DeleteWarning from './DeleteWarning.svelte';
|
||||||
// import ArrowDownThick from '~icons/mdi/arrow-down-thick';
|
// import ArrowDownThick from '~icons/mdi/arrow-down-thick';
|
||||||
import { TRANSPORTATION_TYPES_ICONS } from '$lib';
|
import { TRANSPORTATION_TYPES_ICONS } from '$lib';
|
||||||
|
import { formatDateInTimezone } from '$lib/dateUtils';
|
||||||
|
|
||||||
function getTransportationIcon(type: string) {
|
function getTransportationIcon(type: string) {
|
||||||
if (type in TRANSPORTATION_TYPES_ICONS) {
|
if (type in TRANSPORTATION_TYPES_ICONS) {
|
||||||
|
@ -18,23 +19,6 @@
|
||||||
}
|
}
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
function formatDateInTimezone(utcDate: string, timezone?: string): string {
|
|
||||||
if (!utcDate) return '';
|
|
||||||
try {
|
|
||||||
return new Intl.DateTimeFormat(undefined, {
|
|
||||||
timeZone: timezone,
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
hour12: true
|
|
||||||
}).format(new Date(utcDate));
|
|
||||||
} catch {
|
|
||||||
return new Date(utcDate).toLocaleString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export let transportation: Transportation;
|
export let transportation: Transportation;
|
||||||
export let user: User | null = null;
|
export let user: User | null = null;
|
||||||
export let collection: Collection | null = null;
|
export let collection: Collection | null = null;
|
||||||
|
@ -123,66 +107,62 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div
|
<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"
|
class="card w-full max-w-md bg-base-300 text-base-content shadow-2xl hover:shadow-3xl transition-all duration-300 border border-base-300 hover:border-primary/20 group"
|
||||||
>
|
>
|
||||||
<div class="card-body space-y-4">
|
<div class="card-body p-6 space-y-4">
|
||||||
<!-- Title and Type -->
|
<!-- Title & Mode -->
|
||||||
<h2 class="card-title text-lg font-semibold truncate">{transportation.name}</h2>
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
|
||||||
<div>
|
<h2 class="card-title text-xl font-semibold truncate">{transportation.name}</h2>
|
||||||
<div class="badge badge-secondary">
|
<div class="flex flex-wrap gap-2">
|
||||||
{$t(`transportation.modes.${transportation.type}`) +
|
<div class="badge badge-secondary">
|
||||||
' ' +
|
{$t(`transportation.modes.${transportation.type}`)}
|
||||||
getTransportationIcon(transportation.type)}
|
{' '}{getTransportationIcon(transportation.type)}
|
||||||
|
</div>
|
||||||
|
{#if transportation.type === 'plane' && transportation.flight_number}
|
||||||
|
<div class="badge badge-neutral">{transportation.flight_number}</div>
|
||||||
|
{/if}
|
||||||
|
{#if unlinked}
|
||||||
|
<div class="badge badge-error">{$t('adventures.out_of_range')}</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if transportation.type == 'plane' && transportation.flight_number}
|
|
||||||
<div class="badge badge-neutral-200">{transportation.flight_number}</div>
|
|
||||||
{/if}
|
|
||||||
{#if unlinked}
|
|
||||||
<div class="badge badge-error">{$t('adventures.out_of_range')}</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Locations -->
|
<!-- Start Section -->
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
{#if transportation.from_location}
|
{#if transportation.from_location}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="font-medium text-sm">{$t('adventures.from')}:</span>
|
<span class="text-sm font-medium">{$t('adventures.from')}:</span>
|
||||||
<p class="break-words">{transportation.from_location}</p>
|
<p class="text-sm break-words">{transportation.from_location}</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if transportation.date}
|
{#if transportation.date}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="font-medium text-sm">{$t('adventures.start')}:</span>
|
<span class="text-sm font-medium">{$t('adventures.start')}:</span>
|
||||||
<p>
|
<p class="text-sm">
|
||||||
{formatDateInTimezone(transportation.date, transportation.start_timezone ?? undefined)}
|
{formatDateInTimezone(transportation.date, transportation.start_timezone)}
|
||||||
{#if transportation.start_timezone}
|
{#if transportation.start_timezone}
|
||||||
<span class="text-xs opacity-60 ml-1">({transportation.start_timezone})</span>
|
<span class="ml-1 text-xs opacity-60">({transportation.start_timezone})</span>
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Dates -->
|
<!-- End Section -->
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
{#if transportation.to_location}
|
{#if transportation.to_location}
|
||||||
<!-- <ArrowDownThick class="w-4 h-4" /> -->
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="font-medium text-sm">{$t('adventures.to')}:</span>
|
<span class="text-sm font-medium">{$t('adventures.to')}:</span>
|
||||||
|
<p class="text-sm break-words">{transportation.to_location}</p>
|
||||||
<p class="break-words">{transportation.to_location}</p>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if transportation.end_date}
|
{#if transportation.end_date}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="font-medium text-sm">{$t('adventures.end')}:</span>
|
<span class="text-sm font-medium">{$t('adventures.end')}:</span>
|
||||||
<p>
|
<p class="text-sm">
|
||||||
{formatDateInTimezone(
|
{formatDateInTimezone(transportation.end_date, transportation.end_timezone)}
|
||||||
transportation.end_date,
|
|
||||||
transportation.end_timezone || undefined
|
|
||||||
)}
|
|
||||||
{#if transportation.end_timezone}
|
{#if transportation.end_timezone}
|
||||||
<span class="text-xs opacity-60 ml-1">({transportation.end_timezone})</span>
|
<span class="ml-1 text-xs opacity-60">({transportation.end_timezone})</span>
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -190,20 +170,20 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
{#if transportation.user_id == user?.uuid || (collection && user && collection.shared_with && collection.shared_with.includes(user.uuid))}
|
{#if transportation.user_id == user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))}
|
||||||
<div class="card-actions justify-end">
|
<div class="pt-4 border-t border-base-300 flex justify-end gap-2">
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary btn-sm flex items-center gap-1"
|
class="btn btn-neutral btn-sm flex items-center gap-1"
|
||||||
on:click={editTransportation}
|
on:click={editTransportation}
|
||||||
title="Edit"
|
title={$t('transportation.edit')}
|
||||||
>
|
>
|
||||||
<FileDocumentEdit class="w-5 h-5" />
|
<FileDocumentEdit class="w-5 h-5" />
|
||||||
<span>{$t('transportation.edit')}</span>
|
<span>{$t('transportation.edit')}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
on:click={() => (isWarningModalOpen = true)}
|
|
||||||
class="btn btn-secondary btn-sm flex items-center gap-1"
|
class="btn btn-secondary btn-sm flex items-center gap-1"
|
||||||
title="Delete"
|
on:click={() => (isWarningModalOpen = true)}
|
||||||
|
title={$t('adventures.delete')}
|
||||||
>
|
>
|
||||||
<TrashCanOutline class="w-5 h-5" />
|
<TrashCanOutline class="w-5 h-5" />
|
||||||
<span>{$t('adventures.delete')}</span>
|
<span>{$t('adventures.delete')}</span>
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import type { User } from '$lib/types';
|
import type { User } from '$lib/types';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
import Calendar from '~icons/mdi/calendar';
|
import Calendar from '~icons/mdi/calendar';
|
||||||
|
|
||||||
|
@ -14,49 +15,64 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<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"
|
class="card w-full max-w-xs bg-base-200 text-base-content shadow-lg border border-base-300 hover:shadow-xl transition-all"
|
||||||
>
|
>
|
||||||
<div class="card-body">
|
<div class="card-body items-center text-center space-y-4">
|
||||||
<!-- Profile Picture and User Info -->
|
<!-- Profile Picture -->
|
||||||
<div class="flex flex-col items-center">
|
<div class="avatar">
|
||||||
{#if user.profile_pic}
|
<div class="w-24 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2">
|
||||||
<div class="avatar mb-4">
|
{#if user.profile_pic}
|
||||||
<div class="w-24 rounded-full ring ring-primary ring-offset-neutral ring-offset-2">
|
<img src={user.profile_pic} alt={user.username} />
|
||||||
<img src={user.profile_pic} alt={user.username} />
|
{:else}
|
||||||
|
<div
|
||||||
|
class="bg-base-300 w-full h-full flex items-center justify-center text-xl font-semibold"
|
||||||
|
>
|
||||||
|
{user.first_name?.[0] || user.username?.[0]}{user.last_name?.[0] || user.username?.[1]}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h2 class="card-title text-center text-lg font-bold">
|
<!-- User Info -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-bold leading-tight">
|
||||||
{user.first_name}
|
{user.first_name}
|
||||||
{user.last_name}
|
{user.last_name}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-sm text-center">{user.username}</p>
|
<p class="text-sm opacity-70">@{user.username}</p>
|
||||||
|
|
||||||
<!-- Admin Badge -->
|
|
||||||
{#if user.is_staff}
|
{#if user.is_staff}
|
||||||
<div class="badge badge-primary mt-2">Admin</div>
|
<div class="badge badge-outline badge-primary mt-2">{$t('settings.admin')}</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Member Since -->
|
<!-- Join Date -->
|
||||||
<div class="flex items-center justify-center mt-4 space-x-2 text-sm">
|
<div class="flex items-center gap-2 text-sm text-base-content/70">
|
||||||
<Calendar class="w-5 h-5 text-primary" />
|
<Calendar class="w-4 h-4 text-primary" />
|
||||||
<p>
|
<span>
|
||||||
{user.date_joined ? 'Joined ' + new Date(user.date_joined).toLocaleDateString() : ''}
|
{user.date_joined
|
||||||
</p>
|
? `${$t('adventures.joined')} ` + new Date(user.date_joined).toLocaleDateString()
|
||||||
|
: ''}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Card Actions -->
|
<!-- Actions -->
|
||||||
<div class="card-actions justify-center mt-6">
|
<div class="card-actions w-full justify-center pt-2">
|
||||||
{#if !sharing}
|
{#if !sharing}
|
||||||
<button class="btn btn-primary" on:click={() => goto(`/profile/${user.username}`)}>
|
<button
|
||||||
View Profile
|
class="btn btn-sm btn-primary w-full"
|
||||||
|
on:click={() => goto(`/profile/${user.username}`)}
|
||||||
|
>
|
||||||
|
{$t('adventures.view_profile')}
|
||||||
</button>
|
</button>
|
||||||
{:else if shared_with && !shared_with.includes(user.uuid)}
|
{:else if shared_with && !shared_with.includes(user.uuid)}
|
||||||
<button class="btn btn-success" on:click={() => dispatch('share', user)}> Share </button>
|
<button class="btn btn-sm btn-success w-full" on:click={() => dispatch('share', user)}>
|
||||||
|
{$t('adventures.share')}
|
||||||
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
<button class="btn btn-error" on:click={() => dispatch('unshare', user)}> Unshare </button>
|
<button class="btn btn-sm btn-error w-full" on:click={() => dispatch('unshare', user)}>
|
||||||
|
{$t('adventures.remove')}
|
||||||
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -118,6 +118,23 @@ export function validateDateRange(
|
||||||
return { valid: true };
|
return { valid: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatDateInTimezone(utcDate: string, timezone: string | null): string {
|
||||||
|
if (!utcDate) return '';
|
||||||
|
try {
|
||||||
|
return new Intl.DateTimeFormat(undefined, {
|
||||||
|
timeZone: timezone || undefined,
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: true
|
||||||
|
}).format(new Date(utcDate));
|
||||||
|
} catch {
|
||||||
|
return new Date(utcDate).toLocaleString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format UTC date for display
|
* Format UTC date for display
|
||||||
* @param utcDate - UTC date in ISO format
|
* @param utcDate - UTC date in ISO format
|
||||||
|
|
|
@ -258,6 +258,8 @@
|
||||||
"hide": "Hide",
|
"hide": "Hide",
|
||||||
"clear_location": "Clear Location",
|
"clear_location": "Clear Location",
|
||||||
"starting_airport": "Starting Airport",
|
"starting_airport": "Starting Airport",
|
||||||
|
"view_profile": "View Profile",
|
||||||
|
"joined": "Joined",
|
||||||
"ending_airport": "Ending Airport",
|
"ending_airport": "Ending Airport",
|
||||||
"no_location_found": "No location found",
|
"no_location_found": "No location found",
|
||||||
"from": "From",
|
"from": "From",
|
||||||
|
|
|
@ -319,7 +319,9 @@
|
||||||
"timed": "Cronometrado",
|
"timed": "Cronometrado",
|
||||||
"distance": "Distancia",
|
"distance": "Distancia",
|
||||||
"all_linked_items": "Todos los artículos vinculados",
|
"all_linked_items": "Todos los artículos vinculados",
|
||||||
"itinerary": "Itinerario"
|
"itinerary": "Itinerario",
|
||||||
|
"joined": "Unido",
|
||||||
|
"view_profile": "Ver perfil"
|
||||||
},
|
},
|
||||||
"worldtravel": {
|
"worldtravel": {
|
||||||
"all": "Todo",
|
"all": "Todo",
|
||||||
|
|
|
@ -271,7 +271,9 @@
|
||||||
"timed": "Chronométré",
|
"timed": "Chronométré",
|
||||||
"distance": "Distance",
|
"distance": "Distance",
|
||||||
"all_linked_items": "Tous les éléments liés",
|
"all_linked_items": "Tous les éléments liés",
|
||||||
"itinerary": "Itinéraire"
|
"itinerary": "Itinéraire",
|
||||||
|
"joined": "Joint",
|
||||||
|
"view_profile": "Afficher le profil"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"desc_1": "Découvrez, planifiez et explorez en toute simplicité",
|
"desc_1": "Découvrez, planifiez et explorez en toute simplicité",
|
||||||
|
|
|
@ -271,7 +271,9 @@
|
||||||
"timed": "A tempo",
|
"timed": "A tempo",
|
||||||
"distance": "Distanza",
|
"distance": "Distanza",
|
||||||
"all_linked_items": "Tutti gli elementi collegati",
|
"all_linked_items": "Tutti gli elementi collegati",
|
||||||
"itinerary": "Itinerario"
|
"itinerary": "Itinerario",
|
||||||
|
"joined": "Partecipato",
|
||||||
|
"view_profile": "Visualizza il profilo"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"desc_1": "Scopri, pianifica ed esplora con facilità",
|
"desc_1": "Scopri, pianifica ed esplora con facilità",
|
||||||
|
|
|
@ -271,7 +271,9 @@
|
||||||
"timed": "시간이 정해졌습니다",
|
"timed": "시간이 정해졌습니다",
|
||||||
"distance": "거리",
|
"distance": "거리",
|
||||||
"all_linked_items": "모든 링크 된 항목",
|
"all_linked_items": "모든 링크 된 항목",
|
||||||
"itinerary": "여정"
|
"itinerary": "여정",
|
||||||
|
"joined": "가입",
|
||||||
|
"view_profile": "프로필을 봅니다"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"both_passwords_required": "두 암호 모두 필요합니다",
|
"both_passwords_required": "두 암호 모두 필요합니다",
|
||||||
|
|
|
@ -271,7 +271,9 @@
|
||||||
"timed": "Getimed",
|
"timed": "Getimed",
|
||||||
"distance": "Afstand",
|
"distance": "Afstand",
|
||||||
"all_linked_items": "Alle gekoppelde items",
|
"all_linked_items": "Alle gekoppelde items",
|
||||||
"itinerary": "Routebeschrijving"
|
"itinerary": "Routebeschrijving",
|
||||||
|
"joined": "Samengevoegd",
|
||||||
|
"view_profile": "Bekijk profiel"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"desc_1": "Ontdek, plan en verken met gemak",
|
"desc_1": "Ontdek, plan en verken met gemak",
|
||||||
|
|
|
@ -319,7 +319,9 @@
|
||||||
"timed": "Tidsbestemt",
|
"timed": "Tidsbestemt",
|
||||||
"distance": "Avstand",
|
"distance": "Avstand",
|
||||||
"all_linked_items": "Alle koblede varer",
|
"all_linked_items": "Alle koblede varer",
|
||||||
"itinerary": "Reiserute"
|
"itinerary": "Reiserute",
|
||||||
|
"joined": "Ble med",
|
||||||
|
"view_profile": "Vis profil"
|
||||||
},
|
},
|
||||||
"worldtravel": {
|
"worldtravel": {
|
||||||
"country_list": "Liste over land",
|
"country_list": "Liste over land",
|
||||||
|
|
|
@ -319,7 +319,9 @@
|
||||||
"timed": "Czas",
|
"timed": "Czas",
|
||||||
"distance": "Dystans",
|
"distance": "Dystans",
|
||||||
"all_linked_items": "Wszystkie połączone elementy",
|
"all_linked_items": "Wszystkie połączone elementy",
|
||||||
"itinerary": "Trasa"
|
"itinerary": "Trasa",
|
||||||
|
"joined": "Dołączył",
|
||||||
|
"view_profile": "Zobacz profil"
|
||||||
},
|
},
|
||||||
"worldtravel": {
|
"worldtravel": {
|
||||||
"country_list": "Lista krajów",
|
"country_list": "Lista krajów",
|
||||||
|
|
|
@ -271,7 +271,9 @@
|
||||||
"timed": "Tidsinställd",
|
"timed": "Tidsinställd",
|
||||||
"distance": "Avstånd",
|
"distance": "Avstånd",
|
||||||
"all_linked_items": "Alla länkade objekt",
|
"all_linked_items": "Alla länkade objekt",
|
||||||
"itinerary": "Resväg"
|
"itinerary": "Resväg",
|
||||||
|
"joined": "Gick med i",
|
||||||
|
"view_profile": "Visa profil"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"desc_1": "Upptäck, planera och utforska med lätthet",
|
"desc_1": "Upptäck, planera och utforska med lätthet",
|
||||||
|
|
|
@ -319,7 +319,9 @@
|
||||||
"timed": "时间",
|
"timed": "时间",
|
||||||
"distance": "距离",
|
"distance": "距离",
|
||||||
"all_linked_items": "所有链接的项目",
|
"all_linked_items": "所有链接的项目",
|
||||||
"itinerary": "行程"
|
"itinerary": "行程",
|
||||||
|
"joined": "加入",
|
||||||
|
"view_profile": "查看个人资料"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"forgot_password": "忘记密码?",
|
"forgot_password": "忘记密码?",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue