1
0
Fork 0
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:
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 { .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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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

View file

@ -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",

View file

@ -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",

View file

@ -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é",

View file

@ -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à",

View file

@ -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": "두 암호 모두 필요합니다",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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": "忘记密码?",