1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-07-22 22:39: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:
Sean Morley 2025-05-29 17:47:58 -04:00
parent 3acfc9f228
commit 81006af027
22 changed files with 534 additions and 364 deletions

View file

@ -321,11 +321,8 @@
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.shadow-3xl {
box-shadow: 0 35px 60px -12px rgba(0, 0, 0, 0.25);
}
</style>

View file

@ -62,43 +62,53 @@
on:confirm={deleteChecklist}
/>
{/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 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="flex justify-between">
<h2 class="text-2xl font-semibold -mt-2 break-words text-wrap">
{checklist.name}
</h2>
<div class="card-body p-6 space-y-4">
<!-- Header -->
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
<h2 class="text-xl font-bold break-words">{checklist.name}</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 class="badge badge-primary">{$t('adventures.checklist')}</div>
<!-- Checklist Stats -->
{#if checklist.items.length > 0}
<p>
<p class="text-sm">
{checklist.items.length}
{checklist.items.length > 1 ? $t('checklist.items') : $t('checklist.item')}
</p>
{/if}
{#if unlinked}
<div class="badge badge-error">{$t('adventures.out_of_range')}</div>
{/if}
<!-- Date -->
{#if checklist.date && checklist.date !== ''}
<div class="inline-flex items-center">
<Calendar class="w-5 h-5 mr-1" />
<div class="inline-flex items-center gap-2 text-sm">
<Calendar class="w-5 h-5 text-primary" />
<p>{new Date(checklist.date).toLocaleDateString(undefined, { timeZone: 'UTC' })}</p>
</div>
{/if}
<div class="card-actions justify-end">
<button class="btn btn-neutral-200 mb-2" on:click={editChecklist}>
<Launch class="w-6 h-6" />{$t('notes.open')}
<!-- Actions -->
<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>
{#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
id="delete_adventure"
data-umami-event="Delete Checklist"
class="btn btn-warning"
on:click={() => (isWarningModalOpen = true)}><TrashCan class="w-6 h-6" /></button
class="btn btn-secondary btn-sm flex items-center gap-1"
on:click={() => (isWarningModalOpen = true)}
>
<TrashCan class="w-5 h-5" />
{$t('adventures.delete')}
</button>
{/if}
</div>
</div>

View file

@ -41,25 +41,40 @@
</script>
<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">
<h2 class="card-title overflow-ellipsis">{city.name}</h2>
<div class="card-body p-6 space-y-4">
<!-- Header -->
<h2 class="text-xl font-bold truncate">{city.name}</h2>
<!-- Metadata Badges -->
<div class="flex flex-wrap gap-2">
<div class="badge badge-primary">
{city.region_name}, {city.country_name}
</div>
<div class="badge badge-neutral-300">{city.region}</div>
<div class="badge badge-neutral-300">Region ID: {city.region}</div>
</div>
<div class="card-actions justify-end">
{#if !visited}
<button class="btn btn-primary" on:click={markVisited}
>{$t('adventures.mark_visited')}</button
>
<!-- Actions -->
<div class="pt-4 border-t border-base-300 flex justify-end gap-2">
{#if visited === false}
<button class="btn btn-primary btn-sm" on:click={markVisited}>
{$t('adventures.mark_visited')}
</button>
{/if}
{#if visited}
<button class="btn btn-warning" on:click={removeVisit}>{$t('adventures.remove')}</button>
{#if visited === true}
<button class="btn btn-warning btn-sm" on:click={removeVisit}>
{$t('adventures.remove')}
</button>
{/if}
</div>
</div>
</div>
<style>
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

View file

@ -84,99 +84,156 @@
{/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"
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} />
<div class="card-body">
<div class="flex justify-between">
<button
on:click={() => goto(`/collections/${collection.id}`)}
class="text-2xl font-semibold -mt-2 break-words text-wrap hover:underline"
>
{collection.name}
</button>
</div>
<div class="inline-flex gap-2 mb-2">
<div class="badge badge-secondary">
<!-- Image Carousel -->
<div class="relative overflow-hidden rounded-t-2xl">
<CardCarousel {adventures} />
<!-- Badge Overlay -->
<div class="absolute top-4 left-4 flex flex-col gap-2">
<div class="badge badge-sm badge-secondary shadow-lg">
{collection.is_public ? $t('adventures.public') : $t('adventures.private')}
</div>
{#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}
</div>
<p>{collection.adventures.length} {$t('navbar.adventures')}</p>
{#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>
<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'}
<button class="btn btn-primary" on:click={() => dispatch('link', collection.id)}>
<Plus class="w-5 h-5 mr-1" />
<button class="btn btn-primary btn-block" on:click={() => dispatch('link', collection.id)}>
<Plus class="w-4 h-4" />
{$t('adventures.add_to_collection')}
</button>
{:else}
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-neutral-200">
<DotsHorizontal class="w-6 h-6" />
</div>
<!-- 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"
<div class="flex justify-between items-center">
<button
class="btn btn-neutral btn-sm flex-1 mr-2"
on:click={() => goto(`/collections/${collection.id}`)}
>
{#if type != 'link' && 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 !collection.is_archived}
<button class="btn btn-neutral mb-2" on:click={editAdventure}>
<FileDocumentEdit class="w-6 h-6" />{$t('adventures.edit_collection')}
</button>
<button class="btn btn-neutral mb-2" on:click={() => (isShareModalOpen = true)}>
<ShareVariant class="w-6 h-6" />{$t('adventures.share')}
</button>
<Launch class="w-4 h-4" />
{$t('adventures.open_details')}
</button>
<div class="dropdown dropdown-end">
<button type="button" class="btn btn-square btn-sm btn-base-300">
<DotsHorizontal class="w-5 h-5" />
</button>
<ul
class="dropdown-content menu bg-base-100 rounded-box z-[1] w-64 p-2 shadow-xl border border-base-300"
>
{#if type != 'viewonly'}
<li>
<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 collection.is_archived}
<button class="btn btn-neutral mb-2" on:click={() => archiveCollection(false)}>
<ArchiveArrowUp class="w-6 h-6 mr-1" />{$t('adventures.unarchive')}
</button>
{:else}
<button class="btn btn-neutral mb-2" on:click={() => archiveCollection(true)}>
<ArchiveArrowDown class="w-6 h-6 mr" />{$t('adventures.archive')}
</button>
{#if type == 'viewonly'}
<li>
<button
class="flex items-center gap-2"
on:click={() => goto(`/collections/${collection.id}`)}
>
<Launch class="w-4 h-4" />
{$t('adventures.open_details')}
</button>
</li>
{/if}
<button
id="delete_adventure"
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>
</ul>
</div>
</div>
{/if}
</div>
</div>
</div>
<style>
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>

View file

@ -5,6 +5,7 @@
import { t } from 'svelte-i18n';
import MapMarkerStar from '~icons/mdi/map-marker-star';
import Launch from '~icons/mdi/launch';
export let country: Country;
@ -14,24 +15,31 @@
</script>
<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>
<!-- svelte-ignore a11y-img-redundant-alt -->
<img src={country.flag_url} alt="No image available" class="w-full h-48 object-cover" />
<img src={country.flag_url} alt={`Flag of ${country.name}`} class="w-full h-48 object-cover" />
</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">
{#if country.subregion}
<div class="badge badge-primary">{country.subregion}</div>
{/if}
{#if country.capital}
<div class="badge badge-secondary">
<MapMarkerStar class="-ml-1 mr-1" />{country.capital}
<div class="badge badge-secondary inline-flex items-center gap-1">
<MapMarkerStar class="w-4 h-4" />
{country.capital}
</div>
{/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">
Visited {country.num_visits} Region{country.num_visits > 1 ? 's' : ''}
</div>
@ -42,9 +50,20 @@
{/if}
</div>
<div class="card-actions justify-end">
<!-- <button class="btn btn-info" on:click={moreInfo}>More Info</button> -->
<button class="btn btn-primary" on:click={nav}>{$t('notes.open')}</button>
<!-- Actions -->
<div class="pt-4 border-t border-base-300 flex justify-end">
<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>
<style>
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

View file

@ -7,6 +7,7 @@
import { t } from 'svelte-i18n';
import DeleteWarning from './DeleteWarning.svelte';
import { LODGING_TYPES_ICONS } from '$lib';
import { formatDateInTimezone } from '$lib/dateUtils';
const dispatch = createEventDispatcher();
@ -17,24 +18,6 @@
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 user: User | null = null;
export let collection: Collection | null = null;
@ -109,64 +92,71 @@
{/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"
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">
<!-- Title and Type -->
<h2 class="text-2xl font-semibold">{lodging.name}</h2>
<div>
<div class="badge badge-secondary">
{$t(`lodging.${lodging.type}`) + ' ' + getLodgingIcon(lodging.type)}
<div class="card-body p-6 space-y-4">
<!-- Header -->
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
<h2 class="text-xl font-bold truncate">{lodging.name}</h2>
<div class="flex flex-wrap gap-2">
<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>
{#if unlinked}
<div class="badge badge-error">{$t('adventures.out_of_range')}</div>
{/if}
</div>
<!-- Location -->
<!-- Location Info -->
<div class="space-y-2">
{#if lodging.location}
<div class="flex items-center gap-2">
<span class="font-medium text-sm">{$t('adventures.location')}:</span>
<p>{lodging.location}</p>
<span class="text-sm font-medium">{$t('adventures.location')}:</span>
<p class="text-sm 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.dates')}:</span>
<p>
{formatDateInTimezone(lodging.check_in ?? '', lodging.timezone ?? undefined)}
{formatDateInTimezone(lodging.check_out ?? '', lodging.timezone ?? undefined)}
<span class="text-sm font-medium">{$t('adventures.dates')}:</span>
<p class="text-sm">
{formatDateInTimezone(lodging.check_in, lodging.timezone)}
{formatDateInTimezone(lodging.check_out, 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}
</p>
</div>
{/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}
<div class="flex items-center gap-2">
<span class="font-medium text-sm">{$t('adventures.reservation_number')}:</span>
<p>{lodging.reservation_number}</p>
<span class="text-sm font-medium">{$t('adventures.reservation_number')}:</span>
<p class="text-sm break-all">{lodging.reservation_number}</p>
</div>
{/if}
{#if lodging.price}
<div class="flex items-center gap-2">
<span class="font-medium text-sm">{$t('adventures.price')}:</span>
<p>{lodging.price}</p>
<span class="text-sm font-medium">{$t('adventures.price')}:</span>
<p class="text-sm">{lodging.price}</p>
</div>
{/if}
{/if}
</div>
</div>
{/if}
<!-- Actions -->
{#if lodging.user_id == user?.uuid || (collection && user && collection.shared_with && collection.shared_with.includes(user.uuid))}
<div class="card-actions justify-end">
{#if lodging.user_id == user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))}
<div class="pt-4 border-t border-base-300 flex justify-end gap-2">
<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}
title="Edit"
title={$t('transportation.edit')}
>
<FileDocumentEdit class="w-5 h-5" />
<span>{$t('transportation.edit')}</span>
@ -174,7 +164,7 @@
<button
on:click={() => (isWarningModalOpen = true)}
class="btn btn-secondary btn-sm flex items-center gap-1"
title="Delete"
title={$t('adventures.delete')}
>
<TrashCanOutline class="w-5 h-5" />
<span>{$t('adventures.delete')}</span>

View file

@ -65,63 +65,75 @@
{/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 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="flex justify-between">
<h2 class="text-2xl font-semibold -mt-2 break-words text-wrap">
{note.name}
</h2>
<div class="card-body p-6 space-y-4">
<!-- Header -->
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
<h2 class="text-xl font-bold break-words">{note.name}</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 class="badge badge-primary">{$t('adventures.note')}</div>
{#if unlinked}
<div class="badge badge-error">{$t('adventures.out_of_range')}</div>
{/if}
{#if note.content && note.content.length > 0}
<!-- Note Content -->
{#if note.content && note.content?.length > 0}
<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>
{/if}
{#if note.links && note.links.length > 0}
<p>
{note.links.length}
{note.links.length > 1 ? $t('adventures.links') : $t('adventures.link')}
</p>
<ul class="list-disc pl-6">
{#each note.links.slice(0, 3) as link}
<li>
<a class="link link-primary" href={link}>
{link.split('//')[1].split('/', 1)[0]}
</a>
</li>
{/each}
{#if note.links.length > 3}
<li></li>
{/if}
</ul>
<!-- Links -->
{#if note.links && note.links?.length > 0}
<div class="space-y-1">
<p class="text-sm font-medium">
{note.links.length}
{note.links.length > 1 ? $t('adventures.links') : $t('adventures.link')}
</p>
<ul class="list-disc pl-5 text-sm">
{#each note.links.slice(0, 3) as link}
<li>
<a class="link link-primary" href={link} target="_blank" rel="noopener noreferrer">
{link.split('//')[1]?.split('/', 1)[0]}
</a>
</li>
{/each}
{#if note.links.length > 3}
<li></li>
{/if}
</ul>
</div>
{/if}
<!-- Date -->
{#if note.date && note.date !== ''}
<div class="inline-flex items-center">
<Calendar class="w-5 h-5 mr-1" />
<div class="inline-flex items-center gap-2 text-sm">
<Calendar class="w-5 h-5 text-primary" />
<p>{new Date(note.date).toLocaleDateString(undefined, { timeZone: 'UTC' })}</p>
</div>
{/if}
<div class="card-actions justify-end">
<!-- <button class="btn btn-neutral mb-2" on:click={() => goto(`/notes/${note.id}`)}
><Launch class="w-6 h-6" />Open Details</button
> -->
<button class="btn btn-neutral-200 mb-2" on:click={editNote}>
<Launch class="w-6 h-6" />{$t('notes.open')}
<!-- Actions -->
<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}>
<Launch class="w-5 h-5" />
{$t('notes.open')}
</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
id="delete_adventure"
data-umami-event="Delete Adventure"
class="btn btn-warning"
on:click={() => (isWarningModalOpen = true)}><TrashCan class="w-6 h-6" /></button
class="btn btn-secondary btn-sm flex items-center gap-1"
on:click={() => (isWarningModalOpen = true)}
>
<TrashCan class="w-5 h-5" />
{$t('adventures.delete')}
</button>
{/if}
</div>
</div>

View file

@ -50,36 +50,47 @@
</script>
<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">
<h2 class="card-title overflow-ellipsis">{region.name}</h2>
<div>
<div class="badge badge-primary">
<p>{region.country_name}</p>
</div>
<div class="badge badge-neutral-300">
<p>{region.num_cities} {$t('worldtravel.cities')}</p>
</div>
<div class="badge badge-neutral-300">
<p>{region.id}</p>
<div class="card-body p-6 space-y-4">
<!-- Header -->
<h2 class="text-xl font-bold truncate">{region.name}</h2>
<!-- Metadata Badges -->
<div class="flex flex-wrap gap-2">
<div class="badge badge-primary">{region.country_name}</div>
<div class="badge badge-neutral">
{region.num_cities}
{$t('worldtravel.cities')}
</div>
<div class="badge badge-neutral-300">ID: {region.id}</div>
</div>
<div class="card-actions justify-end">
<!-- <button class="btn btn-info" on:click={moreInfo}>More Info</button> -->
{#if !visited && visited !== undefined}
<button class="btn btn-primary" on:click={markVisited}
>{$t('adventures.mark_visited')}</button
>
<!-- Actions -->
<div class="pt-4 border-t border-base-300 flex flex-wrap gap-2 justify-end">
{#if visited === false}
<button class="btn btn-primary btn-sm" on:click={markVisited}>
{$t('adventures.mark_visited')}
</button>
{/if}
{#if visited && visited !== undefined}
<button class="btn btn-warning" on:click={removeVisit}>{$t('adventures.remove')}</button>
{#if visited === true}
<button class="btn btn-warning btn-sm" on:click={removeVisit}>
{$t('adventures.remove')}
</button>
{/if}
{#if region.num_cities > 0}
<button class="btn btn-neutral-300" on:click={goToCity}
>{$t('worldtravel.view_cities')}</button
>
<button class="btn btn-neutral btn-sm" on:click={goToCity}>
{$t('worldtravel.view_cities')}
</button>
{/if}
</div>
</div>
</div>
<style>
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

View file

@ -89,47 +89,73 @@
</script>
<dialog id="my_modal_1" class="modal">
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div class="modal-box w-11/12 max-w-5xl" role="dialog" on:keydown={handleKeydown} tabindex="0">
<h3 class="font-bold text-lg">{$t('adventures.share')} {collection.name}</h3>
<p class="py-1">{$t('share.share_desc')}</p>
<div class="divider"></div>
<h3 class="font-bold text-md">{$t('share.shared_with')}</h3>
<ul>
{#each sharedWithUsers as user}
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
<UserCard
{user}
shared_with={collection.shared_with}
sharing={true}
on:share={(event) => share(event.detail)}
on:unshare={(event) => unshare(event.detail)}
/>
<div
class="modal-box w-11/12 max-w-5xl p-6 space-y-6"
role="dialog"
tabindex="0"
on:keydown={handleKeydown}
>
<!-- Title -->
<div class="space-y-1">
<h3 class="text-2xl font-bold">
{$t('adventures.share')}
{collection.name}
</h3>
<p class="text-base-content/70">{$t('share.share_desc')}</p>
</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>
{/each}
{#if sharedWithUsers.length === 0}
<p class="text-neutral-content">{$t('share.no_users_shared')}</p>
{:else}
<p class="text-neutral-content italic">{$t('share.no_users_shared')}</p>
{/if}
</ul>
</div>
<div class="divider"></div>
<h3 class="font-bold text-md">{$t('share.not_shared_with')}</h3>
<ul>
{#each notSharedWithUsers as user}
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
<UserCard
{user}
shared_with={collection.shared_with}
sharing={true}
on:share={(event) => share(event.detail)}
on:unshare={(event) => unshare(event.detail)}
/>
<!-- Not Shared With Section -->
<div>
<h4 class="text-lg font-semibold mb-2">{$t('share.not_shared_with')}</h4>
{#if notSharedWithUsers.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 notSharedWithUsers 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>
{/each}
{#if notSharedWithUsers.length === 0}
<p class="text-neutral-content">{$t('share.no_users_shared')}</p>
{:else}
<p class="text-neutral-content italic">{$t('share.no_users_shared')}</p>
{/if}
</ul>
<button class="btn btn-primary mt-4" on:click={close}>{$t('about.close')}</button>
</div>
<!-- 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>
</dialog>

View file

@ -8,6 +8,7 @@
import DeleteWarning from './DeleteWarning.svelte';
// import ArrowDownThick from '~icons/mdi/arrow-down-thick';
import { TRANSPORTATION_TYPES_ICONS } from '$lib';
import { formatDateInTimezone } from '$lib/dateUtils';
function getTransportationIcon(type: string) {
if (type in TRANSPORTATION_TYPES_ICONS) {
@ -18,23 +19,6 @@
}
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 user: User | null = null;
export let collection: Collection | null = null;
@ -123,66 +107,62 @@
{/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"
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">
<!-- Title and Type -->
<h2 class="card-title text-lg font-semibold truncate">{transportation.name}</h2>
<div>
<div class="badge badge-secondary">
{$t(`transportation.modes.${transportation.type}`) +
' ' +
getTransportationIcon(transportation.type)}
<div class="card-body p-6 space-y-4">
<!-- Title & Mode -->
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
<h2 class="card-title text-xl font-semibold truncate">{transportation.name}</h2>
<div class="flex flex-wrap gap-2">
<div class="badge badge-secondary">
{$t(`transportation.modes.${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>
{#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>
<!-- Locations -->
<!-- Start Section -->
<div class="space-y-2">
{#if transportation.from_location}
<div class="flex items-center gap-2">
<span class="font-medium text-sm">{$t('adventures.from')}:</span>
<p class="break-words">{transportation.from_location}</p>
<span class="text-sm font-medium">{$t('adventures.from')}:</span>
<p class="text-sm break-words">{transportation.from_location}</p>
</div>
{/if}
{#if transportation.date}
<div class="flex items-center gap-2">
<span class="font-medium text-sm">{$t('adventures.start')}:</span>
<p>
{formatDateInTimezone(transportation.date, transportation.start_timezone ?? undefined)}
<span class="text-sm font-medium">{$t('adventures.start')}:</span>
<p class="text-sm">
{formatDateInTimezone(transportation.date, 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}
</p>
</div>
{/if}
</div>
<!-- Dates -->
<!-- End Section -->
<div class="space-y-2">
{#if transportation.to_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">{transportation.to_location}</p>
<span class="text-sm font-medium">{$t('adventures.to')}:</span>
<p class="text-sm break-words">{transportation.to_location}</p>
</div>
{/if}
{#if transportation.end_date}
<div class="flex items-center gap-2">
<span class="font-medium text-sm">{$t('adventures.end')}:</span>
<p>
{formatDateInTimezone(
transportation.end_date,
transportation.end_timezone || undefined
)}
<span class="text-sm font-medium">{$t('adventures.end')}:</span>
<p class="text-sm">
{formatDateInTimezone(transportation.end_date, 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}
</p>
</div>
@ -190,20 +170,20 @@
</div>
<!-- Actions -->
{#if transportation.user_id == user?.uuid || (collection && user && collection.shared_with && collection.shared_with.includes(user.uuid))}
<div class="card-actions justify-end">
{#if transportation.user_id == user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))}
<div class="pt-4 border-t border-base-300 flex justify-end gap-2">
<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}
title="Edit"
title={$t('transportation.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"
on:click={() => (isWarningModalOpen = true)}
title={$t('adventures.delete')}
>
<TrashCanOutline class="w-5 h-5" />
<span>{$t('adventures.delete')}</span>

View file

@ -4,6 +4,7 @@
import type { User } from '$lib/types';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
import { t } from 'svelte-i18n';
import Calendar from '~icons/mdi/calendar';
@ -14,49 +15,64 @@
</script>
<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">
<!-- Profile Picture and User Info -->
<div class="flex flex-col items-center">
{#if user.profile_pic}
<div class="avatar mb-4">
<div class="w-24 rounded-full ring ring-primary ring-offset-neutral ring-offset-2">
<img src={user.profile_pic} alt={user.username} />
<div class="card-body items-center text-center space-y-4">
<!-- Profile Picture -->
<div class="avatar">
<div class="w-24 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2">
{#if user.profile_pic}
<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>
{/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.last_name}
</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}
<div class="badge badge-primary mt-2">Admin</div>
<div class="badge badge-outline badge-primary mt-2">{$t('settings.admin')}</div>
{/if}
</div>
<!-- Member Since -->
<div class="flex items-center justify-center mt-4 space-x-2 text-sm">
<Calendar class="w-5 h-5 text-primary" />
<p>
{user.date_joined ? 'Joined ' + new Date(user.date_joined).toLocaleDateString() : ''}
</p>
<!-- Join Date -->
<div class="flex items-center gap-2 text-sm text-base-content/70">
<Calendar class="w-4 h-4 text-primary" />
<span>
{user.date_joined
? `${$t('adventures.joined')} ` + new Date(user.date_joined).toLocaleDateString()
: ''}
</span>
</div>
<!-- Card Actions -->
<div class="card-actions justify-center mt-6">
<!-- Actions -->
<div class="card-actions w-full justify-center pt-2">
{#if !sharing}
<button class="btn btn-primary" on:click={() => goto(`/profile/${user.username}`)}>
View Profile
<button
class="btn btn-sm btn-primary w-full"
on:click={() => goto(`/profile/${user.username}`)}
>
{$t('adventures.view_profile')}
</button>
{: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}
<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}
</div>
</div>