mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-08-05 05:05:17 +02:00
Merge branch 'main' into import-export
This commit is contained in:
commit
afc5fbe5e9
30 changed files with 1800 additions and 328 deletions
|
@ -140,7 +140,7 @@ Hi! I'm Sean, the creator of AdventureLog. I'm a college student and software de
|
||||||
|
|
||||||
## 💎 Acknowledgements
|
## 💎 Acknowledgements
|
||||||
|
|
||||||
- Logo Design by [nordtechtiger](https://github.com/nordtechtiger)
|
- Logo Design by [nordtektiger](https://github.com/nordtektiger)
|
||||||
- WorldTravel Dataset [dr5hn/countries-states-cities-database](https://github.com/dr5hn/countries-states-cities-database)
|
- WorldTravel Dataset [dr5hn/countries-states-cities-database](https://github.com/dr5hn/countries-states-cities-database)
|
||||||
|
|
||||||
### Top Supporters 💖
|
### Top Supporters 💖
|
||||||
|
|
|
@ -270,23 +270,20 @@ class ImmichIntegrationView(viewsets.ViewSet):
|
||||||
integration = get_object_or_404(ImmichIntegration, id=integration_id)
|
integration = get_object_or_404(ImmichIntegration, id=integration_id)
|
||||||
owner_id = integration.user_id
|
owner_id = integration.user_id
|
||||||
|
|
||||||
# Try to find the image entry with collection and sharing information
|
# Try to find the image entry with collections and sharing information
|
||||||
image_entry = (
|
image_entry = (
|
||||||
AdventureImage.objects
|
AdventureImage.objects
|
||||||
.filter(immich_id=imageid, user_id=owner_id)
|
.filter(immich_id=imageid, user_id=owner_id)
|
||||||
.select_related('adventure', 'adventure__collection')
|
.select_related('adventure')
|
||||||
.prefetch_related('adventure__collection__shared_with')
|
.prefetch_related('adventure__collections', 'adventure__collections__shared_with')
|
||||||
.order_by(
|
.order_by('-adventure__is_public') # Public adventures first
|
||||||
'-adventure__is_public', # Public adventures first
|
|
||||||
'-adventure__collection__is_public' # Then public collections
|
|
||||||
)
|
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
|
|
||||||
# Access control
|
# Access control
|
||||||
if image_entry:
|
if image_entry:
|
||||||
adventure = image_entry.adventure
|
adventure = image_entry.adventure
|
||||||
collection = adventure.collection
|
collections = adventure.collections.all()
|
||||||
|
|
||||||
# Determine access level
|
# Determine access level
|
||||||
is_authorized = False
|
is_authorized = False
|
||||||
|
@ -295,17 +292,18 @@ class ImmichIntegrationView(viewsets.ViewSet):
|
||||||
if adventure.is_public:
|
if adventure.is_public:
|
||||||
is_authorized = True
|
is_authorized = True
|
||||||
|
|
||||||
# Level 2: Private adventure in public collection
|
# Level 2: Private adventure in any public collection
|
||||||
elif collection and collection.is_public:
|
elif any(collection.is_public for collection in collections):
|
||||||
is_authorized = True
|
is_authorized = True
|
||||||
|
|
||||||
# Level 3: Owner access
|
# Level 3: Owner access
|
||||||
elif request.user.is_authenticated and request.user.id == owner_id:
|
elif request.user.is_authenticated and request.user.id == owner_id:
|
||||||
is_authorized = True
|
is_authorized = True
|
||||||
|
|
||||||
# Level 4: Shared collection access
|
# Level 4: Shared collection access - check if user has access to any collection
|
||||||
elif (request.user.is_authenticated and collection and
|
elif (request.user.is_authenticated and
|
||||||
collection.shared_with.filter(id=request.user.id).exists()):
|
any(collection.shared_with.filter(id=request.user.id).exists()
|
||||||
|
for collection in collections)):
|
||||||
is_authorized = True
|
is_authorized = True
|
||||||
|
|
||||||
if not is_authorized:
|
if not is_authorized:
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@event-calendar/core": "^3.7.1",
|
"@event-calendar/core": "^3.7.1",
|
||||||
"@event-calendar/day-grid": "^3.7.1",
|
"@event-calendar/day-grid": "^3.7.1",
|
||||||
|
"@event-calendar/interaction": "^3.12.0",
|
||||||
"@event-calendar/time-grid": "^3.7.1",
|
"@event-calendar/time-grid": "^3.7.1",
|
||||||
"@iconify-json/mdi": "^1.1.67",
|
"@iconify-json/mdi": "^1.1.67",
|
||||||
"@sveltejs/adapter-node": "^5.2.0",
|
"@sveltejs/adapter-node": "^5.2.0",
|
||||||
|
|
11
frontend/pnpm-lock.yaml
generated
11
frontend/pnpm-lock.yaml
generated
|
@ -48,6 +48,9 @@ importers:
|
||||||
'@event-calendar/day-grid':
|
'@event-calendar/day-grid':
|
||||||
specifier: ^3.7.1
|
specifier: ^3.7.1
|
||||||
version: 3.12.0
|
version: 3.12.0
|
||||||
|
'@event-calendar/interaction':
|
||||||
|
specifier: ^3.12.0
|
||||||
|
version: 3.12.0
|
||||||
'@event-calendar/time-grid':
|
'@event-calendar/time-grid':
|
||||||
specifier: ^3.7.1
|
specifier: ^3.7.1
|
||||||
version: 3.12.0
|
version: 3.12.0
|
||||||
|
@ -566,6 +569,9 @@ packages:
|
||||||
'@event-calendar/day-grid@3.12.0':
|
'@event-calendar/day-grid@3.12.0':
|
||||||
resolution: {integrity: sha512-gY6XvEIlwWI9uKWsXukyanDmrEWv1UDHdhikhchpe6iZP25p3+760qXIU2kdu91tXjb+hVbpFcn7sdNPPE4u7Q==}
|
resolution: {integrity: sha512-gY6XvEIlwWI9uKWsXukyanDmrEWv1UDHdhikhchpe6iZP25p3+760qXIU2kdu91tXjb+hVbpFcn7sdNPPE4u7Q==}
|
||||||
|
|
||||||
|
'@event-calendar/interaction@3.12.0':
|
||||||
|
resolution: {integrity: sha512-+d3KqxNdcY/RfJrdai37XCoTx7KKpzqJIo/WAjH1p8ZiypsfrHgpWWuTtF76u3hpn/1qqWUM3VFJSTKbjJkWTg==}
|
||||||
|
|
||||||
'@event-calendar/time-grid@3.12.0':
|
'@event-calendar/time-grid@3.12.0':
|
||||||
resolution: {integrity: sha512-n/IoFSq/ym6ad2k+H9RL2A8GpfOJy1zpKKLb1Edp/QEusexpPg8LNdSbxhmKGz6ip5ud0Bi/xgUa8xUqut8ooQ==}
|
resolution: {integrity: sha512-n/IoFSq/ym6ad2k+H9RL2A8GpfOJy1zpKKLb1Edp/QEusexpPg8LNdSbxhmKGz6ip5ud0Bi/xgUa8xUqut8ooQ==}
|
||||||
|
|
||||||
|
@ -2405,6 +2411,11 @@ snapshots:
|
||||||
'@event-calendar/core': 3.12.0
|
'@event-calendar/core': 3.12.0
|
||||||
svelte: 4.2.19
|
svelte: 4.2.19
|
||||||
|
|
||||||
|
'@event-calendar/interaction@3.12.0':
|
||||||
|
dependencies:
|
||||||
|
'@event-calendar/core': 3.12.0
|
||||||
|
svelte: 4.2.19
|
||||||
|
|
||||||
'@event-calendar/time-grid@3.12.0':
|
'@event-calendar/time-grid@3.12.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@event-calendar/core': 3.12.0
|
'@event-calendar/core': 3.12.0
|
||||||
|
|
|
@ -12,8 +12,6 @@
|
||||||
import Search from '~icons/mdi/magnify';
|
import Search from '~icons/mdi/magnify';
|
||||||
import Clear from '~icons/mdi/close';
|
import Clear from '~icons/mdi/close';
|
||||||
import Link from '~icons/mdi/link-variant';
|
import Link from '~icons/mdi/link-variant';
|
||||||
import Filter from '~icons/mdi/filter-variant';
|
|
||||||
import Calendar from '~icons/mdi/calendar';
|
|
||||||
import Check from '~icons/mdi/check-circle';
|
import Check from '~icons/mdi/check-circle';
|
||||||
import Cancel from '~icons/mdi/cancel';
|
import Cancel from '~icons/mdi/cancel';
|
||||||
import Public from '~icons/mdi/earth';
|
import Public from '~icons/mdi/earth';
|
||||||
|
|
|
@ -456,6 +456,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add this collection to the adventure
|
||||||
|
if (collection && collection.id) {
|
||||||
|
adventure.collections = [collection.id];
|
||||||
|
}
|
||||||
|
|
||||||
let res = await fetch('/api/adventures', {
|
let res = await fetch('/api/adventures', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
|
551
frontend/src/lib/components/CollectionAllView.svelte
Normal file
551
frontend/src/lib/components/CollectionAllView.svelte
Normal file
|
@ -0,0 +1,551 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type {
|
||||||
|
Adventure,
|
||||||
|
Transportation,
|
||||||
|
Lodging,
|
||||||
|
Note,
|
||||||
|
Checklist,
|
||||||
|
User,
|
||||||
|
Collection
|
||||||
|
} from '$lib/types';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
// Icons
|
||||||
|
import Adventures from '~icons/mdi/map-marker-path';
|
||||||
|
import TransportationIcon from '~icons/mdi/car';
|
||||||
|
import Hotel from '~icons/mdi/hotel';
|
||||||
|
import NoteIcon from '~icons/mdi/note-text';
|
||||||
|
import ChecklistIcon from '~icons/mdi/check-box-outline';
|
||||||
|
import Search from '~icons/mdi/magnify';
|
||||||
|
import Clear from '~icons/mdi/close';
|
||||||
|
import Filter from '~icons/mdi/filter-variant';
|
||||||
|
|
||||||
|
// Component imports
|
||||||
|
import AdventureCard from './AdventureCard.svelte';
|
||||||
|
import TransportationCard from './TransportationCard.svelte';
|
||||||
|
import LodgingCard from './LodgingCard.svelte';
|
||||||
|
import NoteCard from './NoteCard.svelte';
|
||||||
|
import ChecklistCard from './ChecklistCard.svelte';
|
||||||
|
|
||||||
|
// Props
|
||||||
|
export let adventures: Adventure[] = [];
|
||||||
|
export let transportations: Transportation[] = [];
|
||||||
|
export let lodging: Lodging[] = [];
|
||||||
|
export let notes: Note[] = [];
|
||||||
|
export let checklists: Checklist[] = [];
|
||||||
|
export let user: User | null;
|
||||||
|
export let collection: Collection;
|
||||||
|
|
||||||
|
// State
|
||||||
|
let searchQuery: string = '';
|
||||||
|
let filterOption: string = 'all';
|
||||||
|
let sortOption: string = 'name_asc';
|
||||||
|
|
||||||
|
// Filtered arrays
|
||||||
|
let filteredAdventures: Adventure[] = [];
|
||||||
|
let filteredTransportations: Transportation[] = [];
|
||||||
|
let filteredLodging: Lodging[] = [];
|
||||||
|
let filteredNotes: Note[] = [];
|
||||||
|
let filteredChecklists: Checklist[] = [];
|
||||||
|
|
||||||
|
// Helper function to sort items
|
||||||
|
function sortItems(items: any[], sortOption: string) {
|
||||||
|
const sorted = [...items];
|
||||||
|
|
||||||
|
switch (sortOption) {
|
||||||
|
case 'name_asc':
|
||||||
|
return sorted.sort((a, b) =>
|
||||||
|
(a.name || a.title || '').localeCompare(b.name || b.title || '')
|
||||||
|
);
|
||||||
|
case 'name_desc':
|
||||||
|
return sorted.sort((a, b) =>
|
||||||
|
(b.name || b.title || '').localeCompare(a.name || a.title || '')
|
||||||
|
);
|
||||||
|
case 'date_newest':
|
||||||
|
return sorted.sort(
|
||||||
|
(a, b) => new Date(b.created_at || 0).getTime() - new Date(a.created_at || 0).getTime()
|
||||||
|
);
|
||||||
|
case 'date_oldest':
|
||||||
|
return sorted.sort(
|
||||||
|
(a, b) => new Date(a.created_at || 0).getTime() - new Date(b.created_at || 0).getTime()
|
||||||
|
);
|
||||||
|
case 'visited_first':
|
||||||
|
return sorted.sort((a, b) => {
|
||||||
|
const aVisited = a.visits && a.visits.length > 0;
|
||||||
|
const bVisited = b.visits && b.visits.length > 0;
|
||||||
|
if (aVisited && !bVisited) return -1;
|
||||||
|
if (!aVisited && bVisited) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
case 'unvisited_first':
|
||||||
|
return sorted.sort((a, b) => {
|
||||||
|
const aVisited = a.visits && a.visits.length > 0;
|
||||||
|
const bVisited = b.visits && b.visits.length > 0;
|
||||||
|
if (!aVisited && bVisited) return -1;
|
||||||
|
if (aVisited && !bVisited) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all filters function
|
||||||
|
function clearAllFilters() {
|
||||||
|
searchQuery = '';
|
||||||
|
filterOption = 'all';
|
||||||
|
sortOption = 'name_asc';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reactive statements for filtering and sorting
|
||||||
|
$: {
|
||||||
|
// Filter adventures
|
||||||
|
let filtered = adventures;
|
||||||
|
if (searchQuery !== '') {
|
||||||
|
filtered = filtered.filter((adventure) => {
|
||||||
|
const nameMatch =
|
||||||
|
adventure.name?.toLowerCase().includes(searchQuery.toLowerCase()) || false;
|
||||||
|
const locationMatch =
|
||||||
|
adventure.location?.toLowerCase().includes(searchQuery.toLowerCase()) || false;
|
||||||
|
const descriptionMatch =
|
||||||
|
adventure.description?.toLowerCase().includes(searchQuery.toLowerCase()) || false;
|
||||||
|
return nameMatch || locationMatch || descriptionMatch;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredAdventures = sortItems(filtered, sortOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
// Filter transportations
|
||||||
|
let filtered = transportations;
|
||||||
|
if (searchQuery !== '') {
|
||||||
|
filtered = filtered.filter((transport) => {
|
||||||
|
const nameMatch =
|
||||||
|
transport.name?.toLowerCase().includes(searchQuery.toLowerCase()) || false;
|
||||||
|
const fromMatch =
|
||||||
|
transport.from_location?.toLowerCase().includes(searchQuery.toLowerCase()) || false;
|
||||||
|
const toMatch =
|
||||||
|
transport.to_location?.toLowerCase().includes(searchQuery.toLowerCase()) || false;
|
||||||
|
return nameMatch || fromMatch || toMatch;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredTransportations = sortItems(filtered, sortOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
// Filter lodging
|
||||||
|
let filtered = lodging;
|
||||||
|
if (searchQuery !== '') {
|
||||||
|
filtered = filtered.filter((hotel) => {
|
||||||
|
const nameMatch = hotel.name?.toLowerCase().includes(searchQuery.toLowerCase()) || false;
|
||||||
|
const locationMatch =
|
||||||
|
hotel.location?.toLowerCase().includes(searchQuery.toLowerCase()) || false;
|
||||||
|
return nameMatch || locationMatch;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredLodging = sortItems(filtered, sortOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
// Filter notes
|
||||||
|
let filtered = notes;
|
||||||
|
if (searchQuery !== '') {
|
||||||
|
filtered = filtered.filter((note) => {
|
||||||
|
const titleMatch = note.name?.toLowerCase().includes(searchQuery.toLowerCase()) || false;
|
||||||
|
const contentMatch =
|
||||||
|
note.content?.toLowerCase().includes(searchQuery.toLowerCase()) || false;
|
||||||
|
return titleMatch || contentMatch;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredNotes = sortItems(filtered, sortOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
// Filter checklists
|
||||||
|
let filtered = checklists;
|
||||||
|
if (searchQuery !== '') {
|
||||||
|
filtered = filtered.filter((checklist) => {
|
||||||
|
const titleMatch =
|
||||||
|
checklist.name?.toLowerCase().includes(searchQuery.toLowerCase()) || false;
|
||||||
|
return titleMatch;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredChecklists = sortItems(filtered, sortOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate total items
|
||||||
|
$: totalItems =
|
||||||
|
filteredAdventures.length +
|
||||||
|
filteredTransportations.length +
|
||||||
|
filteredLodging.length +
|
||||||
|
filteredNotes.length +
|
||||||
|
filteredChecklists.length;
|
||||||
|
|
||||||
|
// Event handlers
|
||||||
|
function handleEditAdventure(event: { detail: any }) {
|
||||||
|
dispatch('editAdventure', event.detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteAdventure(event: { detail: any }) {
|
||||||
|
dispatch('deleteAdventure', event.detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEditTransportation(event: { detail: any }) {
|
||||||
|
dispatch('editTransportation', event.detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteTransportation(event: { detail: any }) {
|
||||||
|
dispatch('deleteTransportation', event.detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEditLodging(event: { detail: any }) {
|
||||||
|
dispatch('editLodging', event.detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteLodging(event: { detail: any }) {
|
||||||
|
dispatch('deleteLodging', event.detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEditNote(event: { detail: any }) {
|
||||||
|
dispatch('editNote', event.detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteNote(event: { detail: any }) {
|
||||||
|
dispatch('deleteNote', event.detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEditChecklist(event: { detail: any }) {
|
||||||
|
dispatch('editChecklist', event.detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteChecklist(event: { detail: any }) {
|
||||||
|
dispatch('deleteChecklist', event.detail);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Search and Filter Controls -->
|
||||||
|
<div
|
||||||
|
class="bg-base-100/90 backdrop-blur-lg border border-base-300/50 rounded-2xl p-6 mx-4 mb-6 shadow-lg mt-4"
|
||||||
|
>
|
||||||
|
<!-- Header with Stats -->
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="p-2 bg-primary/10 rounded-xl">
|
||||||
|
<Adventures class="w-6 h-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-bold text-primary">
|
||||||
|
{$t('adventures.collection_contents')}
|
||||||
|
</h2>
|
||||||
|
<p class="text-sm text-base-content/60">
|
||||||
|
{totalItems}
|
||||||
|
{$t('worldtravel.total_items')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Stats -->
|
||||||
|
<div class="hidden md:flex items-center gap-2">
|
||||||
|
<div class="stats stats-horizontal bg-base-200/50 border border-base-300/50">
|
||||||
|
<div class="stat py-2 px-3">
|
||||||
|
<div class="stat-title text-xs">{$t('navbar.adventures')}</div>
|
||||||
|
<div class="stat-value text-sm text-info">{adventures.length}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat py-2 px-3">
|
||||||
|
<div class="stat-title text-xs">{$t('adventures.transportations')}</div>
|
||||||
|
<div class="stat-value text-sm text-warning">{transportations.length}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat py-2 px-3">
|
||||||
|
<div class="stat-title text-xs">{$t('adventures.lodging')}</div>
|
||||||
|
<div class="stat-value text-sm text-success">{lodging.length}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search Bar -->
|
||||||
|
<div class="flex flex-col lg:flex-row items-stretch lg:items-center gap-4 mb-4">
|
||||||
|
<div class="relative flex-1 max-w-md">
|
||||||
|
<Search class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-base-content/40" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="{$t('navbar.search')} {$t('adventures.name_location')}..."
|
||||||
|
class="input input-bordered w-full pl-10 pr-10 bg-base-100/80"
|
||||||
|
bind:value={searchQuery}
|
||||||
|
/>
|
||||||
|
{#if searchQuery.length > 0}
|
||||||
|
<button
|
||||||
|
class="absolute right-3 top-1/2 -translate-y-1/2 text-base-content/40 hover:text-base-content"
|
||||||
|
on:click={() => (searchQuery = '')}
|
||||||
|
>
|
||||||
|
<Clear class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if searchQuery || filterOption !== 'all' || sortOption !== 'name_asc'}
|
||||||
|
<button class="btn btn-ghost btn-sm gap-1" on:click={clearAllFilters}>
|
||||||
|
<Clear class="w-3 h-3" />
|
||||||
|
{$t('worldtravel.clear_all')}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sort Labels (Mobile Friendly) -->
|
||||||
|
<div class="flex flex-wrap gap-2 mb-4">
|
||||||
|
<div class="badge badge-outline gap-1">
|
||||||
|
<Filter class="w-3 h-3" />
|
||||||
|
{$t('adventures.sort')}:
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-1">
|
||||||
|
<button
|
||||||
|
class="badge {sortOption === 'name_asc'
|
||||||
|
? 'badge-primary'
|
||||||
|
: 'badge-ghost'} cursor-pointer hover:badge-primary"
|
||||||
|
on:click={() => (sortOption = 'name_asc')}
|
||||||
|
>
|
||||||
|
A-Z
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="badge {sortOption === 'name_desc'
|
||||||
|
? 'badge-primary'
|
||||||
|
: 'badge-ghost'} cursor-pointer hover:badge-primary"
|
||||||
|
on:click={() => (sortOption = 'name_desc')}
|
||||||
|
>
|
||||||
|
Z-A
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="badge {sortOption === 'date_newest'
|
||||||
|
? 'badge-primary'
|
||||||
|
: 'badge-ghost'} cursor-pointer hover:badge-primary"
|
||||||
|
on:click={() => (sortOption = 'date_newest')}
|
||||||
|
>
|
||||||
|
{$t('worldtravel.newest_first')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="badge {sortOption === 'date_oldest'
|
||||||
|
? 'badge-primary'
|
||||||
|
: 'badge-ghost'} cursor-pointer hover:badge-primary"
|
||||||
|
on:click={() => (sortOption = 'date_oldest')}
|
||||||
|
>
|
||||||
|
{$t('worldtravel.oldest_first')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="badge {sortOption === 'visited_first'
|
||||||
|
? 'badge-primary'
|
||||||
|
: 'badge-ghost'} cursor-pointer hover:badge-primary"
|
||||||
|
on:click={() => (sortOption = 'visited_first')}
|
||||||
|
>
|
||||||
|
{$t('worldtravel.visited_first')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="badge {sortOption === 'unvisited_first'
|
||||||
|
? 'badge-primary'
|
||||||
|
: 'badge-ghost'} cursor-pointer hover:badge-primary"
|
||||||
|
on:click={() => (sortOption = 'unvisited_first')}
|
||||||
|
>
|
||||||
|
{$t('worldtravel.unvisited_first')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter Tabs -->
|
||||||
|
<div class="flex flex-col sm:flex-row sm:items-center gap-2">
|
||||||
|
<span class="text-sm font-medium text-base-content/60">
|
||||||
|
{$t('adventures.show')}:
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Scrollable container on mobile -->
|
||||||
|
<div class="w-full overflow-x-auto">
|
||||||
|
<div class="tabs tabs-boxed bg-base-200 flex-nowrap flex sm:flex-wrap w-max sm:w-auto">
|
||||||
|
<button
|
||||||
|
class="tab tab-sm gap-2 {filterOption === 'all' ? 'tab-active' : ''} whitespace-nowrap"
|
||||||
|
on:click={() => (filterOption = 'all')}
|
||||||
|
>
|
||||||
|
<Adventures class="w-3 h-3" />
|
||||||
|
{$t('adventures.all')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="tab tab-sm gap-2 {filterOption === 'adventures'
|
||||||
|
? 'tab-active'
|
||||||
|
: ''} whitespace-nowrap"
|
||||||
|
on:click={() => (filterOption = 'adventures')}
|
||||||
|
>
|
||||||
|
<Adventures class="w-3 h-3" />
|
||||||
|
{$t('navbar.adventures')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="tab tab-sm gap-2 {filterOption === 'transportation'
|
||||||
|
? 'tab-active'
|
||||||
|
: ''} whitespace-nowrap"
|
||||||
|
on:click={() => (filterOption = 'transportation')}
|
||||||
|
>
|
||||||
|
<TransportationIcon class="w-3 h-3" />
|
||||||
|
{$t('adventures.transportations')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="tab tab-sm gap-2 {filterOption === 'lodging'
|
||||||
|
? 'tab-active'
|
||||||
|
: ''} whitespace-nowrap"
|
||||||
|
on:click={() => (filterOption = 'lodging')}
|
||||||
|
>
|
||||||
|
<Hotel class="w-3 h-3" />
|
||||||
|
{$t('adventures.lodging')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="tab tab-sm gap-2 {filterOption === 'notes' ? 'tab-active' : ''} whitespace-nowrap"
|
||||||
|
on:click={() => (filterOption = 'notes')}
|
||||||
|
>
|
||||||
|
<NoteIcon class="w-3 h-3" />
|
||||||
|
{$t('adventures.notes')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="tab tab-sm gap-2 {filterOption === 'checklists'
|
||||||
|
? 'tab-active'
|
||||||
|
: ''} whitespace-nowrap"
|
||||||
|
on:click={() => (filterOption = 'checklists')}
|
||||||
|
>
|
||||||
|
<ChecklistIcon class="w-3 h-3" />
|
||||||
|
{$t('adventures.checklists')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Adventures Section -->
|
||||||
|
{#if (filterOption === 'all' || filterOption === 'adventures') && filteredAdventures.length > 0}
|
||||||
|
<div class="mb-8">
|
||||||
|
<div class="flex items-center justify-between mx-4 mb-4">
|
||||||
|
<h1 class="text-3xl font-bold text-primary">
|
||||||
|
{$t('adventures.linked_adventures')}
|
||||||
|
</h1>
|
||||||
|
<div class="badge badge-primary badge-lg">{filteredAdventures.length}</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 mx-4">
|
||||||
|
{#each filteredAdventures as adventure}
|
||||||
|
<AdventureCard
|
||||||
|
{user}
|
||||||
|
on:edit={handleEditAdventure}
|
||||||
|
on:delete={handleDeleteAdventure}
|
||||||
|
{adventure}
|
||||||
|
{collection}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Transportation Section -->
|
||||||
|
{#if (filterOption === 'all' || filterOption === 'transportation') && filteredTransportations.length > 0}
|
||||||
|
<div class="mb-8">
|
||||||
|
<div class="flex items-center justify-between mx-4 mb-4">
|
||||||
|
<h1 class="text-3xl font-bold bg-clip-text text-primary">
|
||||||
|
{$t('adventures.transportations')}
|
||||||
|
</h1>
|
||||||
|
<div class="badge badge-warning badge-lg">{filteredTransportations.length}</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 mx-4">
|
||||||
|
{#each filteredTransportations as transportation}
|
||||||
|
<TransportationCard
|
||||||
|
{transportation}
|
||||||
|
{user}
|
||||||
|
on:delete={handleDeleteTransportation}
|
||||||
|
on:edit={handleEditTransportation}
|
||||||
|
{collection}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Lodging Section -->
|
||||||
|
{#if (filterOption === 'all' || filterOption === 'lodging') && filteredLodging.length > 0}
|
||||||
|
<div class="mb-8">
|
||||||
|
<div class="flex items-center justify-between mx-4 mb-4">
|
||||||
|
<h1 class="text-3xl font-bold bg-clip-text text-primary">
|
||||||
|
{$t('adventures.lodging')}
|
||||||
|
</h1>
|
||||||
|
<div class="badge badge-success badge-lg">{filteredLodging.length}</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 mx-4">
|
||||||
|
{#each filteredLodging as hotel}
|
||||||
|
<LodgingCard
|
||||||
|
lodging={hotel}
|
||||||
|
{user}
|
||||||
|
on:delete={handleDeleteLodging}
|
||||||
|
on:edit={handleEditLodging}
|
||||||
|
{collection}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Notes Section -->
|
||||||
|
{#if (filterOption === 'all' || filterOption === 'notes') && filteredNotes.length > 0}
|
||||||
|
<div class="mb-8">
|
||||||
|
<div class="flex items-center justify-between mx-4 mb-4">
|
||||||
|
<h1 class="text-3xl font-bold bg-clip-text text-primary">
|
||||||
|
{$t('adventures.notes')}
|
||||||
|
</h1>
|
||||||
|
<div class="badge badge-info badge-lg">{filteredNotes.length}</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 mx-4">
|
||||||
|
{#each filteredNotes as note}
|
||||||
|
<NoteCard
|
||||||
|
{note}
|
||||||
|
{user}
|
||||||
|
on:edit={handleEditNote}
|
||||||
|
on:delete={handleDeleteNote}
|
||||||
|
{collection}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Checklists Section -->
|
||||||
|
{#if (filterOption === 'all' || filterOption === 'checklists') && filteredChecklists.length > 0}
|
||||||
|
<div class="mb-8">
|
||||||
|
<div class="flex items-center justify-between mx-4 mb-4">
|
||||||
|
<h1 class="text-3xl font-bold bg-clip-text text-primary">
|
||||||
|
{$t('adventures.checklists')}
|
||||||
|
</h1>
|
||||||
|
<div class="badge badge-secondary badge-lg">{filteredChecklists.length}</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 mx-4">
|
||||||
|
{#each filteredChecklists as checklist}
|
||||||
|
<ChecklistCard
|
||||||
|
{checklist}
|
||||||
|
{user}
|
||||||
|
on:delete={handleDeleteChecklist}
|
||||||
|
on:edit={handleEditChecklist}
|
||||||
|
{collection}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
{#if totalItems === 0}
|
||||||
|
<div class="hero min-h-96">
|
||||||
|
<div class="hero-content text-center">
|
||||||
|
<div class="max-w-md">
|
||||||
|
<Clear class="w-16 h-16 text-base-content/30 mb-4" />
|
||||||
|
<h1 class="text-3xl font-bold text-base-content/70">
|
||||||
|
{$t('immich.no_items_found')}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
|
@ -51,6 +51,11 @@
|
||||||
let isEditing = false; // Disable reactivity when editing
|
let isEditing = false; // Disable reactivity when editing
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
// Auto-detect all-day for transportation and lodging types
|
||||||
|
if ((type === 'transportation' || type === 'lodging') && utcStartDate) {
|
||||||
|
allDay = isAllDay(utcStartDate);
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize UTC dates
|
// Initialize UTC dates
|
||||||
localStartDate = updateLocalDate({
|
localStartDate = updateLocalDate({
|
||||||
utcDate: utcStartDate,
|
utcDate: utcStartDate,
|
||||||
|
@ -263,6 +268,8 @@
|
||||||
<label for="date" class="text-sm font-medium">
|
<label for="date" class="text-sm font-medium">
|
||||||
{type === 'transportation'
|
{type === 'transportation'
|
||||||
? $t('adventures.departure_date')
|
? $t('adventures.departure_date')
|
||||||
|
: type === 'lodging'
|
||||||
|
? $t('adventures.check_in')
|
||||||
: $t('adventures.start_date')}
|
: $t('adventures.start_date')}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
@ -295,7 +302,11 @@
|
||||||
{#if localStartDate}
|
{#if localStartDate}
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label for="end_date" class="text-sm font-medium">
|
<label for="end_date" class="text-sm font-medium">
|
||||||
{type === 'transportation' ? $t('adventures.arrival_date') : $t('adventures.end_date')}
|
{type === 'transportation'
|
||||||
|
? $t('adventures.arrival_date')
|
||||||
|
: type === 'lodging'
|
||||||
|
? $t('adventures.check_out')
|
||||||
|
: $t('adventures.end_date')}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{#if allDay}
|
{#if allDay}
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
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';
|
import { formatDateInTimezone } from '$lib/dateUtils';
|
||||||
|
import { formatAllDayDate } from '$lib/dateUtils';
|
||||||
|
import { isAllDay } from '$lib';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
@ -96,8 +98,8 @@
|
||||||
>
|
>
|
||||||
<div class="card-body p-6 space-y-4">
|
<div class="card-body p-6 space-y-4">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
|
<div class="flex flex-col gap-3">
|
||||||
<h2 class="text-xl font-bold truncate">{lodging.name}</h2>
|
<h2 class="text-xl font-bold break-words">{lodging.name}</h2>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<div class="badge badge-secondary">
|
<div class="badge badge-secondary">
|
||||||
{$t(`lodging.${lodging.type}`)}
|
{$t(`lodging.${lodging.type}`)}
|
||||||
|
@ -118,19 +120,40 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if lodging.check_in && lodging.check_out}
|
<div class="space-y-3">
|
||||||
<div class="flex items-center gap-2">
|
{#if lodging.check_in}
|
||||||
<span class="text-sm font-medium">{$t('adventures.dates')}:</span>
|
<div class="flex gap-2 text-sm">
|
||||||
<p class="text-sm">
|
<span class="font-medium whitespace-nowrap">{$t('adventures.check_in')}:</span>
|
||||||
{formatDateInTimezone(lodging.check_in, lodging.timezone)} –
|
<span>
|
||||||
|
{#if isAllDay(lodging.check_in)}
|
||||||
|
{formatAllDayDate(lodging.check_in)}
|
||||||
|
{:else}
|
||||||
|
{formatDateInTimezone(lodging.check_in, lodging.timezone)}
|
||||||
|
{#if lodging.timezone}
|
||||||
|
<span class="ml-1 text-xs opacity-60">({lodging.timezone})</span>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if lodging.check_out}
|
||||||
|
<div class="flex gap-2 text-sm">
|
||||||
|
<span class="font-medium whitespace-nowrap">{$t('adventures.check_out')}:</span>
|
||||||
|
<span>
|
||||||
|
{#if isAllDay(lodging.check_out)}
|
||||||
|
{formatAllDayDate(lodging.check_out)}
|
||||||
|
{:else}
|
||||||
{formatDateInTimezone(lodging.check_out, lodging.timezone)}
|
{formatDateInTimezone(lodging.check_out, lodging.timezone)}
|
||||||
{#if lodging.timezone}
|
{#if lodging.timezone}
|
||||||
<span class="ml-1 text-xs opacity-60">({lodging.timezone})</span>
|
<span class="ml-1 text-xs opacity-60">({lodging.timezone})</span>
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
{/if}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Reservation Info -->
|
<!-- Reservation Info -->
|
||||||
{#if lodging.user_id == user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))}
|
{#if lodging.user_id == user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))}
|
||||||
|
|
|
@ -6,6 +6,9 @@
|
||||||
import type { Collection, Lodging } from '$lib/types';
|
import type { Collection, Lodging } from '$lib/types';
|
||||||
import LocationDropdown from './LocationDropdown.svelte';
|
import LocationDropdown from './LocationDropdown.svelte';
|
||||||
import DateRangeCollapse from './DateRangeCollapse.svelte';
|
import DateRangeCollapse from './DateRangeCollapse.svelte';
|
||||||
|
import { isAllDay } from '$lib';
|
||||||
|
// @ts-ignore
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
@ -22,19 +25,7 @@
|
||||||
label: string;
|
label: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const LODGING_TYPES: LodgingType[] = [
|
let lodgingTimezone: string | undefined = lodging.timezone ?? undefined;
|
||||||
{ value: 'hotel', label: 'Hotel' },
|
|
||||||
{ value: 'hostel', label: 'Hostel' },
|
|
||||||
{ value: 'resort', label: 'Resort' },
|
|
||||||
{ value: 'bnb', label: 'Bed & Breakfast' },
|
|
||||||
{ value: 'campground', label: 'Campground' },
|
|
||||||
{ value: 'cabin', label: 'Cabin' },
|
|
||||||
{ value: 'apartment', label: 'Apartment' },
|
|
||||||
{ value: 'house', label: 'House' },
|
|
||||||
{ value: 'villa', label: 'Villa' },
|
|
||||||
{ value: 'motel', label: 'Motel' },
|
|
||||||
{ value: 'other', label: 'Other' }
|
|
||||||
];
|
|
||||||
|
|
||||||
// Initialize hotel with values from lodgingToEdit or default values
|
// Initialize hotel with values from lodgingToEdit or default values
|
||||||
function initializeLodging(lodgingToEdit: Lodging | null): Lodging {
|
function initializeLodging(lodgingToEdit: Lodging | null): Lodging {
|
||||||
|
@ -94,6 +85,25 @@
|
||||||
async function handleSubmit(event: Event) {
|
async function handleSubmit(event: Event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
lodging.timezone = lodgingTimezone || null;
|
||||||
|
|
||||||
|
// Auto-set end date if missing but start date exists
|
||||||
|
// If check_out is not set, we will set it to the next day at 9:00 AM in the lodging's timezone if it is a timed event. If it is an all-day event, we will set it to the next day at UTC 00:00:00.
|
||||||
|
if (lodging.check_in && !lodging.check_out) {
|
||||||
|
if (isAllDay(lodging.check_in)) {
|
||||||
|
// For all-day, just add one day and keep at UTC 00:00:00
|
||||||
|
const start = DateTime.fromISO(lodging.check_in, { zone: 'utc' });
|
||||||
|
const nextDay = start.plus({ days: 1 });
|
||||||
|
lodging.check_out = nextDay.toISO();
|
||||||
|
} else {
|
||||||
|
// For timed events, set to next day at 9:00 AM in lodging's timezone, then convert to UTC
|
||||||
|
const start = DateTime.fromISO(lodging.check_in, { zone: lodging.timezone || 'utc' });
|
||||||
|
const nextDay = start.plus({ days: 1 });
|
||||||
|
const end = nextDay.set({ hour: 9, minute: 0, second: 0, millisecond: 0 });
|
||||||
|
lodging.check_out = end.toUTC().toISO();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create or update lodging...
|
// Create or update lodging...
|
||||||
const url = lodging.id === '' ? '/api/lodging' : `/api/lodging/${lodging.id}`;
|
const url = lodging.id === '' ? '/api/lodging' : `/api/lodging/${lodging.id}`;
|
||||||
const method = lodging.id === '' ? 'POST' : 'PATCH';
|
const method = lodging.id === '' ? 'POST' : 'PATCH';
|
||||||
|
@ -304,7 +314,8 @@
|
||||||
type="lodging"
|
type="lodging"
|
||||||
bind:utcStartDate={lodging.check_in}
|
bind:utcStartDate={lodging.check_in}
|
||||||
bind:utcEndDate={lodging.check_out}
|
bind:utcEndDate={lodging.check_out}
|
||||||
bind:selectedStartTimezone={lodging.timezone}
|
bind:selectedStartTimezone={lodgingTimezone}
|
||||||
|
{collection}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Location Information -->
|
<!-- Location Information -->
|
||||||
|
|
|
@ -8,7 +8,8 @@
|
||||||
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';
|
import { formatAllDayDate, formatDateInTimezone } from '$lib/dateUtils';
|
||||||
|
import { isAllDay } from '$lib';
|
||||||
|
|
||||||
function getTransportationIcon(type: string) {
|
function getTransportationIcon(type: string) {
|
||||||
if (type in TRANSPORTATION_TYPES_ICONS) {
|
if (type in TRANSPORTATION_TYPES_ICONS) {
|
||||||
|
@ -161,10 +162,14 @@
|
||||||
<div class="flex gap-2 text-sm">
|
<div class="flex gap-2 text-sm">
|
||||||
<span class="font-medium whitespace-nowrap">{$t('adventures.start')}:</span>
|
<span class="font-medium whitespace-nowrap">{$t('adventures.start')}:</span>
|
||||||
<span>
|
<span>
|
||||||
|
{#if isAllDay(transportation.date) && (!transportation.end_date || isAllDay(transportation.end_date))}
|
||||||
|
{formatAllDayDate(transportation.date)}
|
||||||
|
{:else}
|
||||||
{formatDateInTimezone(transportation.date, transportation.start_timezone)}
|
{formatDateInTimezone(transportation.date, transportation.start_timezone)}
|
||||||
{#if transportation.start_timezone}
|
{#if transportation.start_timezone}
|
||||||
<span class="ml-1 text-xs opacity-60">({transportation.start_timezone})</span>
|
<span class="ml-1 text-xs opacity-60">({transportation.start_timezone})</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -173,10 +178,14 @@
|
||||||
<div class="flex gap-2 text-sm">
|
<div class="flex gap-2 text-sm">
|
||||||
<span class="font-medium whitespace-nowrap">{$t('adventures.end')}:</span>
|
<span class="font-medium whitespace-nowrap">{$t('adventures.end')}:</span>
|
||||||
<span>
|
<span>
|
||||||
|
{#if isAllDay(transportation.end_date) && (!transportation.date || isAllDay(transportation.date))}
|
||||||
|
{formatAllDayDate(transportation.end_date)}
|
||||||
|
{:else}
|
||||||
{formatDateInTimezone(transportation.end_date, transportation.end_timezone)}
|
{formatDateInTimezone(transportation.end_date, transportation.end_timezone)}
|
||||||
{#if transportation.end_timezone}
|
{#if transportation.end_timezone}
|
||||||
<span class="ml-1 text-xs opacity-60">({transportation.end_timezone})</span>
|
<span class="ml-1 text-xs opacity-60">({transportation.end_timezone})</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export let appVersion = 'v0.10.0-main-06152025';
|
export let appVersion = 'v0.10.0-main-06192025';
|
||||||
export let versionChangelog = 'https://github.com/seanmorley15/AdventureLog/releases/tag/v0.10.0';
|
export let versionChangelog = 'https://github.com/seanmorley15/AdventureLog/releases/tag/v0.10.0';
|
||||||
export let appTitle = 'AdventureLog';
|
export let appTitle = 'AdventureLog';
|
||||||
export let copyrightYear = '2023-2025';
|
export let copyrightYear = '2023-2025';
|
||||||
|
|
|
@ -147,6 +147,25 @@ export function formatUTCDate(utcDate: string | null): string {
|
||||||
return dateTime.toISO()?.slice(0, 16).replace('T', ' ') || '';
|
return dateTime.toISO()?.slice(0, 16).replace('T', ' ') || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format all-day date for display without timezone conversion
|
||||||
|
* @param dateString - Date string in ISO format (YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS)
|
||||||
|
* @returns Formatted date string (e.g., "Jun 1, 2025")
|
||||||
|
*/
|
||||||
|
export function formatAllDayDate(dateString: string): string {
|
||||||
|
if (!dateString) return '';
|
||||||
|
|
||||||
|
// Extract just the date part and add midday time to avoid timezone issues
|
||||||
|
const datePart = dateString.split('T')[0];
|
||||||
|
const dateWithMidday = `${datePart}T12:00:00`;
|
||||||
|
|
||||||
|
return new Intl.DateTimeFormat('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
}).format(new Date(dateWithMidday));
|
||||||
|
}
|
||||||
|
|
||||||
export const VALID_TIMEZONES = [
|
export const VALID_TIMEZONES = [
|
||||||
'Africa/Abidjan',
|
'Africa/Abidjan',
|
||||||
'Africa/Accra',
|
'Africa/Accra',
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import inspirationalQuotes from './json/quotes.json';
|
import inspirationalQuotes from './json/quotes.json';
|
||||||
import randomBackgrounds from './json/backgrounds.json';
|
import randomBackgrounds from './json/backgrounds.json';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
import type {
|
import type {
|
||||||
Adventure,
|
Adventure,
|
||||||
Background,
|
Background,
|
||||||
|
@ -67,32 +70,50 @@ export function groupAdventuresByDate(
|
||||||
): Record<string, Adventure[]> {
|
): Record<string, Adventure[]> {
|
||||||
const groupedAdventures: Record<string, Adventure[]> = {};
|
const groupedAdventures: Record<string, Adventure[]> = {};
|
||||||
|
|
||||||
// Initialize all days in the range
|
// Initialize all days in the range using DateTime
|
||||||
for (let i = 0; i < numberOfDays; i++) {
|
for (let i = 0; i < numberOfDays; i++) {
|
||||||
const currentDate = new Date(startDate);
|
const currentDate = DateTime.fromJSDate(startDate).plus({ days: i });
|
||||||
currentDate.setDate(startDate.getDate() + i);
|
const dateString = currentDate.toISODate(); // 'YYYY-MM-DD'
|
||||||
const dateString = getLocalDateString(currentDate);
|
|
||||||
groupedAdventures[dateString] = [];
|
groupedAdventures[dateString] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
adventures.forEach((adventure) => {
|
adventures.forEach((adventure) => {
|
||||||
adventure.visits.forEach((visit) => {
|
adventure.visits.forEach((visit) => {
|
||||||
if (visit.start_date) {
|
if (visit.start_date) {
|
||||||
// Check if this is an all-day event (both start and end at midnight)
|
// Check if it's all-day: start has 00:00:00 AND (no end OR end also has 00:00:00)
|
||||||
const isAllDayEvent =
|
const startHasZeros = isAllDay(visit.start_date);
|
||||||
isAllDay(visit.start_date) && (visit.end_date ? isAllDay(visit.end_date) : false);
|
const endHasZeros = visit.end_date ? isAllDay(visit.end_date) : true;
|
||||||
|
const isAllDayEvent = startHasZeros && endHasZeros;
|
||||||
|
|
||||||
// For all-day events, we need to handle dates differently
|
let startDT: DateTime;
|
||||||
if (isAllDayEvent && visit.end_date) {
|
let endDT: DateTime;
|
||||||
// Extract just the date parts without time
|
|
||||||
const startDateStr = visit.start_date.split('T')[0];
|
|
||||||
const endDateStr = visit.end_date.split('T')[0];
|
|
||||||
|
|
||||||
// Loop through all days in the range
|
if (isAllDayEvent) {
|
||||||
|
// For all-day events, extract just the date part and ignore timezone
|
||||||
|
const dateOnly = visit.start_date.split('T')[0]; // Get 'YYYY-MM-DD'
|
||||||
|
startDT = DateTime.fromISO(dateOnly); // This creates a date without time/timezone
|
||||||
|
|
||||||
|
endDT = visit.end_date ? DateTime.fromISO(visit.end_date.split('T')[0]) : startDT;
|
||||||
|
} else {
|
||||||
|
// For timed events, use timezone conversion
|
||||||
|
startDT = DateTime.fromISO(visit.start_date, {
|
||||||
|
zone: visit.timezone ?? 'UTC'
|
||||||
|
});
|
||||||
|
|
||||||
|
endDT = visit.end_date
|
||||||
|
? DateTime.fromISO(visit.end_date, {
|
||||||
|
zone: visit.timezone ?? 'UTC'
|
||||||
|
})
|
||||||
|
: startDT;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startDateStr = startDT.toISODate();
|
||||||
|
const endDateStr = endDT.toISODate();
|
||||||
|
|
||||||
|
// Loop through all days in range
|
||||||
for (let i = 0; i < numberOfDays; i++) {
|
for (let i = 0; i < numberOfDays; i++) {
|
||||||
const currentDate = new Date(startDate);
|
const currentDate = DateTime.fromJSDate(startDate).plus({ days: i });
|
||||||
currentDate.setDate(startDate.getDate() + i);
|
const currentDateStr = currentDate.toISODate();
|
||||||
const currentDateStr = getLocalDateString(currentDate);
|
|
||||||
|
|
||||||
// Include the current day if it falls within the adventure date range
|
// Include the current day if it falls within the adventure date range
|
||||||
if (currentDateStr >= startDateStr && currentDateStr <= endDateStr) {
|
if (currentDateStr >= startDateStr && currentDateStr <= endDateStr) {
|
||||||
|
@ -101,35 +122,6 @@ export function groupAdventuresByDate(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Handle regular events with time components
|
|
||||||
const adventureStartDate = new Date(visit.start_date);
|
|
||||||
const adventureDateStr = getLocalDateString(adventureStartDate);
|
|
||||||
|
|
||||||
if (visit.end_date) {
|
|
||||||
const adventureEndDate = new Date(visit.end_date);
|
|
||||||
const endDateStr = getLocalDateString(adventureEndDate);
|
|
||||||
|
|
||||||
// Loop through all days and include adventure if it falls within the range
|
|
||||||
for (let i = 0; i < numberOfDays; i++) {
|
|
||||||
const currentDate = new Date(startDate);
|
|
||||||
currentDate.setDate(startDate.getDate() + i);
|
|
||||||
const dateString = getLocalDateString(currentDate);
|
|
||||||
|
|
||||||
// Include the current day if it falls within the adventure date range
|
|
||||||
if (dateString >= adventureDateStr && dateString <= endDateStr) {
|
|
||||||
if (groupedAdventures[dateString]) {
|
|
||||||
groupedAdventures[dateString].push(adventure);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If there's no end date, add adventure to the start date only
|
|
||||||
if (groupedAdventures[adventureDateStr]) {
|
|
||||||
groupedAdventures[adventureDateStr].push(adventure);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -144,13 +136,6 @@ function getLocalDateString(date: Date): string {
|
||||||
return `${year}-${month}-${day}`;
|
return `${year}-${month}-${day}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to check if a given date string represents midnight (all-day)
|
|
||||||
// Improved isAllDay function to handle different ISO date formats
|
|
||||||
export function isAllDay(dateStr: string): boolean {
|
|
||||||
// Check for various midnight formats in UTC
|
|
||||||
return dateStr.endsWith('T00:00:00Z') || dateStr.endsWith('T00:00:00.000Z');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function groupTransportationsByDate(
|
export function groupTransportationsByDate(
|
||||||
transportations: Transportation[],
|
transportations: Transportation[],
|
||||||
startDate: Date,
|
startDate: Date,
|
||||||
|
@ -158,82 +143,127 @@ export function groupTransportationsByDate(
|
||||||
): Record<string, Transportation[]> {
|
): Record<string, Transportation[]> {
|
||||||
const groupedTransportations: Record<string, Transportation[]> = {};
|
const groupedTransportations: Record<string, Transportation[]> = {};
|
||||||
|
|
||||||
// Initialize all days in the range
|
// Initialize days
|
||||||
for (let i = 0; i < numberOfDays; i++) {
|
for (let i = 0; i < numberOfDays; i++) {
|
||||||
const currentDate = new Date(startDate);
|
const currentDate = DateTime.fromJSDate(startDate).plus({ days: i });
|
||||||
currentDate.setDate(startDate.getDate() + i);
|
const dateString = currentDate.toISODate(); // 'YYYY-MM-DD'
|
||||||
const dateString = getLocalDateString(currentDate);
|
|
||||||
groupedTransportations[dateString] = [];
|
groupedTransportations[dateString] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
transportations.forEach((transportation) => {
|
transportations.forEach((transportation) => {
|
||||||
if (transportation.date) {
|
if (transportation.date) {
|
||||||
const transportationDate = getLocalDateString(new Date(transportation.date));
|
// Check if it's all-day: start has 00:00:00 AND (no end OR end also has 00:00:00)
|
||||||
if (transportation.end_date) {
|
const startHasZeros = transportation.date.includes('T00:00:00');
|
||||||
const endDate = new Date(transportation.end_date).toISOString().split('T')[0];
|
const endHasZeros = transportation.end_date
|
||||||
|
? transportation.end_date.includes('T00:00:00')
|
||||||
|
: true;
|
||||||
|
const isTranspoAllDay = startHasZeros && endHasZeros;
|
||||||
|
|
||||||
// Loop through all days and include transportation if it falls within the range
|
let startDT: DateTime;
|
||||||
|
let endDT: DateTime;
|
||||||
|
|
||||||
|
if (isTranspoAllDay) {
|
||||||
|
// For all-day events, extract just the date part and ignore timezone
|
||||||
|
const dateOnly = transportation.date.split('T')[0]; // Get 'YYYY-MM-DD'
|
||||||
|
startDT = DateTime.fromISO(dateOnly); // This creates a date without time/timezone
|
||||||
|
|
||||||
|
endDT = transportation.end_date
|
||||||
|
? DateTime.fromISO(transportation.end_date.split('T')[0])
|
||||||
|
: startDT;
|
||||||
|
} else {
|
||||||
|
// For timed events, use timezone conversion
|
||||||
|
startDT = DateTime.fromISO(transportation.date, {
|
||||||
|
zone: transportation.start_timezone ?? 'UTC'
|
||||||
|
});
|
||||||
|
|
||||||
|
endDT = transportation.end_date
|
||||||
|
? DateTime.fromISO(transportation.end_date, {
|
||||||
|
zone: transportation.end_timezone ?? transportation.start_timezone ?? 'UTC'
|
||||||
|
})
|
||||||
|
: startDT;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startDateStr = startDT.toISODate();
|
||||||
|
const endDateStr = endDT.toISODate();
|
||||||
|
|
||||||
|
// Loop through all days in range
|
||||||
for (let i = 0; i < numberOfDays; i++) {
|
for (let i = 0; i < numberOfDays; i++) {
|
||||||
const currentDate = new Date(startDate);
|
const currentDate = DateTime.fromJSDate(startDate).plus({ days: i });
|
||||||
currentDate.setDate(startDate.getDate() + i);
|
const currentDateStr = currentDate.toISODate();
|
||||||
const dateString = getLocalDateString(currentDate);
|
|
||||||
|
|
||||||
// Include the current day if it falls within the transportation date range
|
if (currentDateStr >= startDateStr && currentDateStr <= endDateStr) {
|
||||||
if (dateString >= transportationDate && dateString <= endDate) {
|
groupedTransportations[currentDateStr]?.push(transportation);
|
||||||
if (groupedTransportations[dateString]) {
|
|
||||||
groupedTransportations[dateString].push(transportation);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (groupedTransportations[transportationDate]) {
|
|
||||||
// If there's no end date, add transportation to the start date only
|
|
||||||
groupedTransportations[transportationDate].push(transportation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return groupedTransportations;
|
return groupedTransportations;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function groupLodgingByDate(
|
export function groupLodgingByDate(
|
||||||
transportations: Lodging[],
|
lodging: Lodging[],
|
||||||
startDate: Date,
|
startDate: Date,
|
||||||
numberOfDays: number
|
numberOfDays: number
|
||||||
): Record<string, Lodging[]> {
|
): Record<string, Lodging[]> {
|
||||||
const groupedTransportations: Record<string, Lodging[]> = {};
|
const groupedLodging: Record<string, Lodging[]> = {};
|
||||||
|
|
||||||
// Initialize all days in the range using local dates
|
// Initialize days (excluding last day for lodging)
|
||||||
for (let i = 0; i < numberOfDays; i++) {
|
// If trip is 7/1 to 7/4 (4 days), show lodging only on 7/1, 7/2, 7/3
|
||||||
const currentDate = new Date(startDate);
|
const lodgingDays = numberOfDays - 1;
|
||||||
currentDate.setDate(startDate.getDate() + i);
|
|
||||||
const dateString = getLocalDateString(currentDate);
|
for (let i = 0; i < lodgingDays; i++) {
|
||||||
groupedTransportations[dateString] = [];
|
const currentDate = DateTime.fromJSDate(startDate).plus({ days: i });
|
||||||
|
const dateString = currentDate.toISODate(); // 'YYYY-MM-DD'
|
||||||
|
groupedLodging[dateString] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
transportations.forEach((transportation) => {
|
lodging.forEach((hotel) => {
|
||||||
if (transportation.check_in) {
|
if (hotel.check_in) {
|
||||||
// Use local date string conversion
|
// Check if it's all-day: start has 00:00:00 AND (no end OR end also has 00:00:00)
|
||||||
const transportationDate = getLocalDateString(new Date(transportation.check_in));
|
const startHasZeros = hotel.check_in.includes('T00:00:00');
|
||||||
if (transportation.check_out) {
|
const endHasZeros = hotel.check_out ? hotel.check_out.includes('T00:00:00') : true;
|
||||||
const endDate = getLocalDateString(new Date(transportation.check_out));
|
const isAllDay = startHasZeros && endHasZeros;
|
||||||
|
|
||||||
// Loop through all days and include transportation if it falls within the transportation date range
|
let startDT: DateTime;
|
||||||
for (let i = 0; i < numberOfDays; i++) {
|
let endDT: DateTime;
|
||||||
const currentDate = new Date(startDate);
|
|
||||||
currentDate.setDate(startDate.getDate() + i);
|
|
||||||
const dateString = getLocalDateString(currentDate);
|
|
||||||
|
|
||||||
if (dateString >= transportationDate && dateString <= endDate) {
|
if (isAllDay) {
|
||||||
groupedTransportations[dateString].push(transportation);
|
// For all-day events, extract just the date part and ignore timezone
|
||||||
|
const dateOnly = hotel.check_in.split('T')[0]; // Get 'YYYY-MM-DD'
|
||||||
|
startDT = DateTime.fromISO(dateOnly); // This creates a date without time/timezone
|
||||||
|
|
||||||
|
endDT = hotel.check_out ? DateTime.fromISO(hotel.check_out.split('T')[0]) : startDT;
|
||||||
|
} else {
|
||||||
|
// For timed events, use timezone conversion
|
||||||
|
startDT = DateTime.fromISO(hotel.check_in, {
|
||||||
|
zone: hotel.timezone ?? 'UTC'
|
||||||
|
});
|
||||||
|
|
||||||
|
endDT = hotel.check_out
|
||||||
|
? DateTime.fromISO(hotel.check_out, {
|
||||||
|
zone: hotel.timezone ?? 'UTC'
|
||||||
|
})
|
||||||
|
: startDT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const startDateStr = startDT.toISODate();
|
||||||
|
const endDateStr = endDT.toISODate();
|
||||||
|
|
||||||
|
// Loop through lodging days only (excluding last day)
|
||||||
|
for (let i = 0; i < lodgingDays; i++) {
|
||||||
|
const currentDate = DateTime.fromJSDate(startDate).plus({ days: i });
|
||||||
|
const currentDateStr = currentDate.toISODate();
|
||||||
|
|
||||||
|
// Show lodging on days where check-in occurs through the day before check-out
|
||||||
|
// For lodging, we typically want to show it on the nights you're staying
|
||||||
|
if (currentDateStr >= startDateStr && currentDateStr < endDateStr) {
|
||||||
|
groupedLodging[currentDateStr]?.push(hotel);
|
||||||
}
|
}
|
||||||
} else if (groupedTransportations[transportationDate]) {
|
|
||||||
groupedTransportations[transportationDate].push(transportation);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return groupedTransportations;
|
return groupedLodging;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function groupNotesByDate(
|
export function groupNotesByDate(
|
||||||
|
@ -292,6 +322,13 @@ export function groupChecklistsByDate(
|
||||||
return groupedChecklists;
|
return groupedChecklists;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper to check if a given date string represents midnight (all-day)
|
||||||
|
// Improved isAllDay function to handle different ISO date formats
|
||||||
|
export function isAllDay(dateStr: string): boolean {
|
||||||
|
// Check for various midnight formats in UTC
|
||||||
|
return dateStr.endsWith('T00:00:00Z') || dateStr.endsWith('T00:00:00.000Z');
|
||||||
|
}
|
||||||
|
|
||||||
export function continentCodeToString(code: string) {
|
export function continentCodeToString(code: string) {
|
||||||
switch (code) {
|
switch (code) {
|
||||||
case 'AF':
|
case 'AF':
|
||||||
|
|
|
@ -243,7 +243,10 @@
|
||||||
"delete_collection_warning": "Sind Sie sicher, dass Sie diese Sammlung löschen möchten? \nDiese Aktion kann nicht rückgängig gemacht werden.",
|
"delete_collection_warning": "Sind Sie sicher, dass Sie diese Sammlung löschen möchten? \nDiese Aktion kann nicht rückgängig gemacht werden.",
|
||||||
"done": "Erledigt",
|
"done": "Erledigt",
|
||||||
"loading_adventures": "Ladeabenteuer ...",
|
"loading_adventures": "Ladeabenteuer ...",
|
||||||
"name_location": "Name, Ort"
|
"name_location": "Name, Ort",
|
||||||
|
"collection_contents": "Sammelinhalt",
|
||||||
|
"check_in": "Einchecken",
|
||||||
|
"check_out": "Kasse"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"desc_1": "Entdecken, planen und erkunden Sie mühelos",
|
"desc_1": "Entdecken, planen und erkunden Sie mühelos",
|
||||||
|
@ -371,7 +374,12 @@
|
||||||
"show_map_labels": "Kartenbezeichnungen anzeigen",
|
"show_map_labels": "Kartenbezeichnungen anzeigen",
|
||||||
"total_cities": "Gesamtstädte",
|
"total_cities": "Gesamtstädte",
|
||||||
"total_countries": "Gesamtländer",
|
"total_countries": "Gesamtländer",
|
||||||
"total_regions": "Gesamtregionen"
|
"total_regions": "Gesamtregionen",
|
||||||
|
"newest_first": "Neuester zuerst",
|
||||||
|
"oldest_first": "Älteste zuerst",
|
||||||
|
"unvisited_first": "Zuerst nicht besucht",
|
||||||
|
"visited_first": "Zuerst besucht",
|
||||||
|
"total_items": "Gesamtartikel"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"account_settings": "Benutzerkonto",
|
"account_settings": "Benutzerkonto",
|
||||||
|
@ -685,5 +693,19 @@
|
||||||
},
|
},
|
||||||
"google_maps": {
|
"google_maps": {
|
||||||
"google_maps_integration_desc": "Verbinden Sie Ihr Google Maps-Konto, um hochwertige Suchergebnisse und Empfehlungen für Standort zu erhalten."
|
"google_maps_integration_desc": "Verbinden Sie Ihr Google Maps-Konto, um hochwertige Suchergebnisse und Empfehlungen für Standort zu erhalten."
|
||||||
|
},
|
||||||
|
"calendar": {
|
||||||
|
"all_categories": "Alle Kategorien",
|
||||||
|
"all_day_event": "Ganztägige Veranstaltung",
|
||||||
|
"calendar_overview": "Kalenderübersicht",
|
||||||
|
"categories": "Kategorien",
|
||||||
|
"day": "Tag",
|
||||||
|
"events_scheduled": "Veranstaltungen geplant",
|
||||||
|
"filter_by_category": "Filter nach Kategorie",
|
||||||
|
"filtered_results": "Gefilterte Ergebnisse",
|
||||||
|
"month": "Monat",
|
||||||
|
"today": "Heute",
|
||||||
|
"total_events": "Gesamtereignisse",
|
||||||
|
"week": "Woche"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,6 +98,8 @@
|
||||||
"latitude": "Latitude",
|
"latitude": "Latitude",
|
||||||
"visit": "Visit",
|
"visit": "Visit",
|
||||||
"timed": "Timed",
|
"timed": "Timed",
|
||||||
|
"check_in": "Check In",
|
||||||
|
"check_out": "Check Out",
|
||||||
"coordinates": "Coordinates",
|
"coordinates": "Coordinates",
|
||||||
"copy_coordinates": "Copy Coordinates",
|
"copy_coordinates": "Copy Coordinates",
|
||||||
"visits": "Visits",
|
"visits": "Visits",
|
||||||
|
@ -220,6 +222,7 @@
|
||||||
"no_adventures_to_recommendations": "No adventures found. Add at least one adventure to get recommendations.",
|
"no_adventures_to_recommendations": "No adventures found. Add at least one adventure to get recommendations.",
|
||||||
"display_name": "Display Name",
|
"display_name": "Display Name",
|
||||||
"adventure_not_found": "There are no adventures to display. Add some using the plus button at the bottom right or try changing filters!",
|
"adventure_not_found": "There are no adventures to display. Add some using the plus button at the bottom right or try changing filters!",
|
||||||
|
"collection_contents": "Collection Contents",
|
||||||
"no_adventures_found": "No adventures found",
|
"no_adventures_found": "No adventures found",
|
||||||
"no_adventures_message": "Start documenting your adventures and planning new ones. Every journey has a story worth telling.",
|
"no_adventures_message": "Start documenting your adventures and planning new ones. Every journey has a story worth telling.",
|
||||||
"mark_visited": "Mark Visited",
|
"mark_visited": "Mark Visited",
|
||||||
|
@ -337,6 +340,7 @@
|
||||||
"filter_by_region": "Filter by Region",
|
"filter_by_region": "Filter by Region",
|
||||||
"all_regions": "All Regions",
|
"all_regions": "All Regions",
|
||||||
"clear_all_filters": "Clear All Filters",
|
"clear_all_filters": "Clear All Filters",
|
||||||
|
"total_items": "Total Items",
|
||||||
"filter_by": "Filter by",
|
"filter_by": "Filter by",
|
||||||
"interactive_map": "Interactive Map",
|
"interactive_map": "Interactive Map",
|
||||||
"no_regions_found": "No regions found",
|
"no_regions_found": "No regions found",
|
||||||
|
@ -346,7 +350,11 @@
|
||||||
"show_map_labels": "Show Map Labels",
|
"show_map_labels": "Show Map Labels",
|
||||||
"hide_map_labels": "Hide Map Labels",
|
"hide_map_labels": "Hide Map Labels",
|
||||||
"total_cities": "Total Cities",
|
"total_cities": "Total Cities",
|
||||||
"region_completed": "Region completed"
|
"region_completed": "Region completed",
|
||||||
|
"newest_first": "Newest First",
|
||||||
|
"oldest_first": "Oldest First",
|
||||||
|
"visited_first": "Visited First",
|
||||||
|
"unvisited_first": "Unvisited First"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
|
@ -685,5 +693,19 @@
|
||||||
"adventure_recommendations": "Adventure Recommendations",
|
"adventure_recommendations": "Adventure Recommendations",
|
||||||
"food": "Food",
|
"food": "Food",
|
||||||
"tourism": "Tourism"
|
"tourism": "Tourism"
|
||||||
|
},
|
||||||
|
"calendar": {
|
||||||
|
"today": "Today",
|
||||||
|
"month": "Month",
|
||||||
|
"week": "Week",
|
||||||
|
"day": "Day",
|
||||||
|
"events_scheduled": "events scheduled",
|
||||||
|
"total_events": "Total Events",
|
||||||
|
"all_categories": "All Categories",
|
||||||
|
"calendar_overview": "Calendar Overview",
|
||||||
|
"categories": "Categories",
|
||||||
|
"filtered_results": "Filtered Results",
|
||||||
|
"filter_by_category": "Filter by Category",
|
||||||
|
"all_day_event": "All Day Event"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -295,7 +295,10 @@
|
||||||
"delete_collection_warning": "¿Estás seguro de que quieres eliminar esta colección? \nEsta acción no se puede deshacer.",
|
"delete_collection_warning": "¿Estás seguro de que quieres eliminar esta colección? \nEsta acción no se puede deshacer.",
|
||||||
"done": "Hecho",
|
"done": "Hecho",
|
||||||
"loading_adventures": "Cargando aventuras ...",
|
"loading_adventures": "Cargando aventuras ...",
|
||||||
"name_location": "Nombre, ubicación"
|
"name_location": "Nombre, ubicación",
|
||||||
|
"collection_contents": "Contenido de la colección",
|
||||||
|
"check_in": "Registrarse",
|
||||||
|
"check_out": "Verificar"
|
||||||
},
|
},
|
||||||
"worldtravel": {
|
"worldtravel": {
|
||||||
"all": "Todo",
|
"all": "Todo",
|
||||||
|
@ -346,7 +349,12 @@
|
||||||
"show_map_labels": "Mostrar etiquetas de mapa",
|
"show_map_labels": "Mostrar etiquetas de mapa",
|
||||||
"total_regions": "Total de regiones",
|
"total_regions": "Total de regiones",
|
||||||
"region_completed": "Región completada",
|
"region_completed": "Región completada",
|
||||||
"total_cities": "Ciudades totales"
|
"total_cities": "Ciudades totales",
|
||||||
|
"newest_first": "El primero primero",
|
||||||
|
"oldest_first": "El más antiguo primero",
|
||||||
|
"unvisited_first": "Primero no visitado",
|
||||||
|
"visited_first": "Visitado primero",
|
||||||
|
"total_items": "Total de artículos"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"forgot_password": "¿Has olvidado tu contraseña?",
|
"forgot_password": "¿Has olvidado tu contraseña?",
|
||||||
|
@ -685,5 +693,19 @@
|
||||||
},
|
},
|
||||||
"google_maps": {
|
"google_maps": {
|
||||||
"google_maps_integration_desc": "Conecte su cuenta de Google Maps para obtener resultados y recomendaciones de búsqueda de ubicación de alta calidad."
|
"google_maps_integration_desc": "Conecte su cuenta de Google Maps para obtener resultados y recomendaciones de búsqueda de ubicación de alta calidad."
|
||||||
|
},
|
||||||
|
"calendar": {
|
||||||
|
"all_categories": "Todas las categorías",
|
||||||
|
"all_day_event": "Evento todo el día",
|
||||||
|
"calendar_overview": "Descripción general del calendario",
|
||||||
|
"categories": "Categorías",
|
||||||
|
"day": "Día",
|
||||||
|
"events_scheduled": "Eventos programados",
|
||||||
|
"filter_by_category": "Filtrar por categoría",
|
||||||
|
"filtered_results": "Resultados filtrados",
|
||||||
|
"month": "Mes",
|
||||||
|
"today": "Hoy",
|
||||||
|
"total_events": "Total de eventos",
|
||||||
|
"week": "Semana"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -243,7 +243,10 @@
|
||||||
"delete_collection_warning": "Êtes-vous sûr de vouloir supprimer cette collection? \nCette action ne peut pas être annulée.",
|
"delete_collection_warning": "Êtes-vous sûr de vouloir supprimer cette collection? \nCette action ne peut pas être annulée.",
|
||||||
"done": "Fait",
|
"done": "Fait",
|
||||||
"loading_adventures": "Chargement des aventures ...",
|
"loading_adventures": "Chargement des aventures ...",
|
||||||
"name_location": "nom, emplacement"
|
"name_location": "nom, emplacement",
|
||||||
|
"collection_contents": "Contenu de la collection",
|
||||||
|
"check_in": "Enregistrement",
|
||||||
|
"check_out": "Vérifier"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"desc_1": "Découvrez, planifiez et explorez en toute simplicité",
|
"desc_1": "Découvrez, planifiez et explorez en toute simplicité",
|
||||||
|
@ -371,7 +374,12 @@
|
||||||
"show_map_labels": "Afficher les étiquettes de carte",
|
"show_map_labels": "Afficher les étiquettes de carte",
|
||||||
"total_cities": "Total des villes",
|
"total_cities": "Total des villes",
|
||||||
"total_countries": "Total des pays",
|
"total_countries": "Total des pays",
|
||||||
"total_regions": "Régions totales"
|
"total_regions": "Régions totales",
|
||||||
|
"newest_first": "Le plus récent premier",
|
||||||
|
"oldest_first": "Le plus ancien premier",
|
||||||
|
"unvisited_first": "Sans visité d'abord",
|
||||||
|
"visited_first": "Visité en premier",
|
||||||
|
"total_items": "Total des articles"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"account_settings": "Paramètres du compte utilisateur",
|
"account_settings": "Paramètres du compte utilisateur",
|
||||||
|
@ -685,5 +693,19 @@
|
||||||
},
|
},
|
||||||
"google_maps": {
|
"google_maps": {
|
||||||
"google_maps_integration_desc": "Connectez votre compte Google Maps pour obtenir des résultats de recherche et recommandations de recherche de haute qualité."
|
"google_maps_integration_desc": "Connectez votre compte Google Maps pour obtenir des résultats de recherche et recommandations de recherche de haute qualité."
|
||||||
|
},
|
||||||
|
"calendar": {
|
||||||
|
"all_categories": "Toutes les catégories",
|
||||||
|
"all_day_event": "Événement toute la journée",
|
||||||
|
"calendar_overview": "Aperçu du calendrier",
|
||||||
|
"categories": "Catégories",
|
||||||
|
"day": "Jour",
|
||||||
|
"events_scheduled": "événements prévus",
|
||||||
|
"filter_by_category": "Filtre par catégorie",
|
||||||
|
"filtered_results": "Résultats filtrés",
|
||||||
|
"month": "Mois",
|
||||||
|
"today": "Aujourd'hui",
|
||||||
|
"total_events": "Événements totaux",
|
||||||
|
"week": "Semaine"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -243,7 +243,10 @@
|
||||||
"delete_collection_warning": "Sei sicuro di voler eliminare questa collezione? \nQuesta azione non può essere annullata.",
|
"delete_collection_warning": "Sei sicuro di voler eliminare questa collezione? \nQuesta azione non può essere annullata.",
|
||||||
"done": "Fatto",
|
"done": "Fatto",
|
||||||
"loading_adventures": "Caricamento di avventure ...",
|
"loading_adventures": "Caricamento di avventure ...",
|
||||||
"name_location": "Nome, posizione"
|
"name_location": "Nome, posizione",
|
||||||
|
"collection_contents": "Contenuto di raccolta",
|
||||||
|
"check_in": "Check -in",
|
||||||
|
"check_out": "Guardare"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"desc_1": "Scopri, pianifica ed esplora con facilità",
|
"desc_1": "Scopri, pianifica ed esplora con facilità",
|
||||||
|
@ -371,7 +374,12 @@
|
||||||
"show_map_labels": "Mostra etichette mappe",
|
"show_map_labels": "Mostra etichette mappe",
|
||||||
"total_cities": "Città totali",
|
"total_cities": "Città totali",
|
||||||
"total_countries": "Paesi totali",
|
"total_countries": "Paesi totali",
|
||||||
"total_regions": "Regioni totali"
|
"total_regions": "Regioni totali",
|
||||||
|
"newest_first": "Primo il più recente",
|
||||||
|
"oldest_first": "Prima più antico",
|
||||||
|
"unvisited_first": "Non visitato per primo",
|
||||||
|
"visited_first": "Visitato per primo",
|
||||||
|
"total_items": "Articoli totali"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"account_settings": "Impostazioni dell'account utente",
|
"account_settings": "Impostazioni dell'account utente",
|
||||||
|
@ -685,5 +693,19 @@
|
||||||
},
|
},
|
||||||
"google_maps": {
|
"google_maps": {
|
||||||
"google_maps_integration_desc": "Collega il tuo account Google Maps per ottenere risultati e consigli di ricerca sulla posizione di alta qualità."
|
"google_maps_integration_desc": "Collega il tuo account Google Maps per ottenere risultati e consigli di ricerca sulla posizione di alta qualità."
|
||||||
|
},
|
||||||
|
"calendar": {
|
||||||
|
"all_categories": "Tutte le categorie",
|
||||||
|
"all_day_event": "Evento per tutto il giorno",
|
||||||
|
"calendar_overview": "Panoramica del calendario",
|
||||||
|
"categories": "Categorie",
|
||||||
|
"day": "Giorno",
|
||||||
|
"events_scheduled": "eventi programmati",
|
||||||
|
"filter_by_category": "Filtro per categoria",
|
||||||
|
"filtered_results": "Risultati filtrati",
|
||||||
|
"month": "Mese",
|
||||||
|
"today": "Oggi",
|
||||||
|
"total_events": "Eventi totali",
|
||||||
|
"week": "Settimana"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -243,7 +243,10 @@
|
||||||
"delete_collection_warning": "이 컬렉션을 삭제 하시겠습니까? \n이 조치는 취소 할 수 없습니다.",
|
"delete_collection_warning": "이 컬렉션을 삭제 하시겠습니까? \n이 조치는 취소 할 수 없습니다.",
|
||||||
"done": "완료",
|
"done": "완료",
|
||||||
"loading_adventures": "적재 모험 ...",
|
"loading_adventures": "적재 모험 ...",
|
||||||
"name_location": "이름, 위치"
|
"name_location": "이름, 위치",
|
||||||
|
"collection_contents": "수집 내용",
|
||||||
|
"check_in": "체크인",
|
||||||
|
"check_out": "체크 아웃"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"confirm_password": "비밀번호 확인",
|
"confirm_password": "비밀번호 확인",
|
||||||
|
@ -663,7 +666,12 @@
|
||||||
"show_map_labels": "지도 레이블 표시",
|
"show_map_labels": "지도 레이블 표시",
|
||||||
"total_cities": "총 도시",
|
"total_cities": "총 도시",
|
||||||
"total_countries": "총 국가",
|
"total_countries": "총 국가",
|
||||||
"total_regions": "총 지역"
|
"total_regions": "총 지역",
|
||||||
|
"newest_first": "최신 첫 번째",
|
||||||
|
"oldest_first": "가장 오래된 첫 번째",
|
||||||
|
"unvisited_first": "먼저 방문하지 않습니다",
|
||||||
|
"visited_first": "먼저 방문했습니다",
|
||||||
|
"total_items": "총 항목"
|
||||||
},
|
},
|
||||||
"lodging": {
|
"lodging": {
|
||||||
"apartment": "아파트",
|
"apartment": "아파트",
|
||||||
|
@ -684,5 +692,19 @@
|
||||||
},
|
},
|
||||||
"google_maps": {
|
"google_maps": {
|
||||||
"google_maps_integration_desc": "Google지도 계정을 연결하여 고품질 위치 검색 결과 및 권장 사항을 얻으십시오."
|
"google_maps_integration_desc": "Google지도 계정을 연결하여 고품질 위치 검색 결과 및 권장 사항을 얻으십시오."
|
||||||
|
},
|
||||||
|
"calendar": {
|
||||||
|
"all_categories": "모든 카테고리",
|
||||||
|
"all_day_event": "하루 종일 이벤트",
|
||||||
|
"calendar_overview": "캘린더 개요",
|
||||||
|
"categories": "카테고리",
|
||||||
|
"day": "낮",
|
||||||
|
"events_scheduled": "예약 된 이벤트",
|
||||||
|
"filter_by_category": "카테고리 별 필터",
|
||||||
|
"filtered_results": "필터링 된 결과",
|
||||||
|
"month": "월",
|
||||||
|
"today": "오늘",
|
||||||
|
"total_events": "총 이벤트",
|
||||||
|
"week": "주"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -243,7 +243,10 @@
|
||||||
"delete_collection_warning": "Weet u zeker dat u deze collectie wilt verwijderen? \nDeze actie kan niet ongedaan worden gemaakt.",
|
"delete_collection_warning": "Weet u zeker dat u deze collectie wilt verwijderen? \nDeze actie kan niet ongedaan worden gemaakt.",
|
||||||
"done": "Klaar",
|
"done": "Klaar",
|
||||||
"loading_adventures": "Adventuren laden ...",
|
"loading_adventures": "Adventuren laden ...",
|
||||||
"name_location": "naam, locatie"
|
"name_location": "naam, locatie",
|
||||||
|
"collection_contents": "Verzamelingsinhoud",
|
||||||
|
"check_in": "Inchecken",
|
||||||
|
"check_out": "Uitchecken"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"desc_1": "Ontdek, plan en verken met gemak",
|
"desc_1": "Ontdek, plan en verken met gemak",
|
||||||
|
@ -371,7 +374,12 @@
|
||||||
"show_map_labels": "Toon kaartlabels",
|
"show_map_labels": "Toon kaartlabels",
|
||||||
"total_cities": "Totale steden",
|
"total_cities": "Totale steden",
|
||||||
"total_countries": "Totale landen",
|
"total_countries": "Totale landen",
|
||||||
"total_regions": "Totaal aantal regio's"
|
"total_regions": "Totaal aantal regio's",
|
||||||
|
"newest_first": "Nieuwste eerste",
|
||||||
|
"oldest_first": "Oudste eerste",
|
||||||
|
"unvisited_first": "Eerst niet bezocht",
|
||||||
|
"visited_first": "Eerst bezocht",
|
||||||
|
"total_items": "Totale items"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"account_settings": "Gebruikersaccount instellingen",
|
"account_settings": "Gebruikersaccount instellingen",
|
||||||
|
@ -685,5 +693,19 @@
|
||||||
},
|
},
|
||||||
"google_maps": {
|
"google_maps": {
|
||||||
"google_maps_integration_desc": "Sluit uw Google Maps-account aan om zoekresultaten en aanbevelingen van hoge kwaliteit te krijgen."
|
"google_maps_integration_desc": "Sluit uw Google Maps-account aan om zoekresultaten en aanbevelingen van hoge kwaliteit te krijgen."
|
||||||
|
},
|
||||||
|
"calendar": {
|
||||||
|
"all_categories": "Alle categorieën",
|
||||||
|
"all_day_event": "De hele dag evenement",
|
||||||
|
"calendar_overview": "Kalenderoverzicht",
|
||||||
|
"categories": "Categorieën",
|
||||||
|
"day": "Dag",
|
||||||
|
"events_scheduled": "geplande evenementen",
|
||||||
|
"filter_by_category": "Filter per categorie",
|
||||||
|
"filtered_results": "Gefilterde resultaten",
|
||||||
|
"month": "Maand",
|
||||||
|
"today": "Vandaag",
|
||||||
|
"total_events": "Totale gebeurtenissen",
|
||||||
|
"week": "Week"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -295,7 +295,10 @@
|
||||||
"delete_collection_warning": "Er du sikker på at du vil slette denne samlingen? \nDenne handlingen kan ikke angres.",
|
"delete_collection_warning": "Er du sikker på at du vil slette denne samlingen? \nDenne handlingen kan ikke angres.",
|
||||||
"done": "Ferdig",
|
"done": "Ferdig",
|
||||||
"loading_adventures": "Laster opp eventyr ...",
|
"loading_adventures": "Laster opp eventyr ...",
|
||||||
"name_location": "Navn, plassering"
|
"name_location": "Navn, plassering",
|
||||||
|
"collection_contents": "Samlingsinnhold",
|
||||||
|
"check_in": "Sjekk inn",
|
||||||
|
"check_out": "Sjekk ut"
|
||||||
},
|
},
|
||||||
"worldtravel": {
|
"worldtravel": {
|
||||||
"country_list": "Liste over land",
|
"country_list": "Liste over land",
|
||||||
|
@ -346,7 +349,12 @@
|
||||||
"show_map_labels": "Vis kartetiketter",
|
"show_map_labels": "Vis kartetiketter",
|
||||||
"total_cities": "Totalt byer",
|
"total_cities": "Totalt byer",
|
||||||
"total_countries": "Totalt land",
|
"total_countries": "Totalt land",
|
||||||
"total_regions": "Totale regioner"
|
"total_regions": "Totale regioner",
|
||||||
|
"newest_first": "Nyeste først",
|
||||||
|
"oldest_first": "Eldste først",
|
||||||
|
"unvisited_first": "Uvisitert først",
|
||||||
|
"visited_first": "Besøkte først",
|
||||||
|
"total_items": "Totalt gjenstander"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"username": "Brukernavn",
|
"username": "Brukernavn",
|
||||||
|
@ -685,5 +693,19 @@
|
||||||
},
|
},
|
||||||
"google_maps": {
|
"google_maps": {
|
||||||
"google_maps_integration_desc": "Koble til Google Maps-kontoen din for å få søkeresultater og anbefalinger av høy kvalitet."
|
"google_maps_integration_desc": "Koble til Google Maps-kontoen din for å få søkeresultater og anbefalinger av høy kvalitet."
|
||||||
|
},
|
||||||
|
"calendar": {
|
||||||
|
"all_categories": "Alle kategorier",
|
||||||
|
"all_day_event": "Hele dagens arrangement",
|
||||||
|
"calendar_overview": "Kalenderoversikt",
|
||||||
|
"categories": "Kategorier",
|
||||||
|
"day": "Dag",
|
||||||
|
"events_scheduled": "hendelser planlagt",
|
||||||
|
"filter_by_category": "Filter etter kategori",
|
||||||
|
"filtered_results": "Filtrerte resultater",
|
||||||
|
"month": "Måned",
|
||||||
|
"today": "I dag",
|
||||||
|
"total_events": "Total hendelser",
|
||||||
|
"week": "Uke"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -295,7 +295,10 @@
|
||||||
"done": "Zrobione",
|
"done": "Zrobione",
|
||||||
"loading_adventures": "Ładowanie przygód ...",
|
"loading_adventures": "Ładowanie przygód ...",
|
||||||
"name_location": "Nazwa, lokalizacja",
|
"name_location": "Nazwa, lokalizacja",
|
||||||
"delete_collection_warning": "Czy na pewno chcesz usunąć tę kolekcję? \nTego działania nie można cofnąć."
|
"delete_collection_warning": "Czy na pewno chcesz usunąć tę kolekcję? \nTego działania nie można cofnąć.",
|
||||||
|
"collection_contents": "Zawartość kolekcji",
|
||||||
|
"check_in": "Zameldować się",
|
||||||
|
"check_out": "Wymeldować się"
|
||||||
},
|
},
|
||||||
"worldtravel": {
|
"worldtravel": {
|
||||||
"country_list": "Lista krajów",
|
"country_list": "Lista krajów",
|
||||||
|
@ -346,7 +349,12 @@
|
||||||
"total_countries": "Kraje ogółem",
|
"total_countries": "Kraje ogółem",
|
||||||
"total_regions": "Regiony ogółem",
|
"total_regions": "Regiony ogółem",
|
||||||
"all_regions": "Wszystkie regiony",
|
"all_regions": "Wszystkie regiony",
|
||||||
"cities_in": "Miasta w"
|
"cities_in": "Miasta w",
|
||||||
|
"newest_first": "Najnowszy pierwszy",
|
||||||
|
"oldest_first": "Najstarszy pierwszy",
|
||||||
|
"unvisited_first": "Najpierw niewidziane",
|
||||||
|
"visited_first": "Odwiedziłem pierwszy",
|
||||||
|
"total_items": "Całkowite przedmioty"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"username": "Nazwa użytkownika",
|
"username": "Nazwa użytkownika",
|
||||||
|
@ -685,5 +693,19 @@
|
||||||
},
|
},
|
||||||
"google_maps": {
|
"google_maps": {
|
||||||
"google_maps_integration_desc": "Połącz swoje konto Google Maps, aby uzyskać wysokiej jakości wyniki wyszukiwania i zalecenia dotyczące lokalizacji."
|
"google_maps_integration_desc": "Połącz swoje konto Google Maps, aby uzyskać wysokiej jakości wyniki wyszukiwania i zalecenia dotyczące lokalizacji."
|
||||||
|
},
|
||||||
|
"calendar": {
|
||||||
|
"all_categories": "Wszystkie kategorie",
|
||||||
|
"all_day_event": "Wydarzenie przez cały dzień",
|
||||||
|
"calendar_overview": "Przegląd kalendarza",
|
||||||
|
"categories": "Kategorie",
|
||||||
|
"day": "Dzień",
|
||||||
|
"events_scheduled": "Zaplanowane wydarzenia",
|
||||||
|
"filter_by_category": "Filtr według kategorii",
|
||||||
|
"filtered_results": "Przefiltrowane wyniki",
|
||||||
|
"month": "Miesiąc",
|
||||||
|
"today": "Dzisiaj",
|
||||||
|
"total_events": "Całkowite zdarzenia",
|
||||||
|
"week": "Tydzień"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -295,7 +295,10 @@
|
||||||
"delete_collection_warning": "Вы уверены, что хотите удалить эту коллекцию? \nЭто действие не может быть отменено.",
|
"delete_collection_warning": "Вы уверены, что хотите удалить эту коллекцию? \nЭто действие не может быть отменено.",
|
||||||
"done": "Сделанный",
|
"done": "Сделанный",
|
||||||
"loading_adventures": "Загрузка приключений ...",
|
"loading_adventures": "Загрузка приключений ...",
|
||||||
"name_location": "имя, местоположение"
|
"name_location": "имя, местоположение",
|
||||||
|
"collection_contents": "Содержание коллекции",
|
||||||
|
"check_in": "Регистрироваться",
|
||||||
|
"check_out": "Проверить"
|
||||||
},
|
},
|
||||||
"worldtravel": {
|
"worldtravel": {
|
||||||
"country_list": "Список стран",
|
"country_list": "Список стран",
|
||||||
|
@ -346,7 +349,12 @@
|
||||||
"show_map_labels": "Показать этикетки карты",
|
"show_map_labels": "Показать этикетки карты",
|
||||||
"total_cities": "Общие города",
|
"total_cities": "Общие города",
|
||||||
"total_countries": "Всего стран",
|
"total_countries": "Всего стран",
|
||||||
"total_regions": "Общие регионы"
|
"total_regions": "Общие регионы",
|
||||||
|
"newest_first": "Новейший первый",
|
||||||
|
"oldest_first": "Сначала старейший",
|
||||||
|
"unvisited_first": "Не заселяется первым",
|
||||||
|
"visited_first": "Посетил первым",
|
||||||
|
"total_items": "Общие предметы"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"username": "Имя пользователя",
|
"username": "Имя пользователя",
|
||||||
|
@ -685,5 +693,19 @@
|
||||||
"adventure_recommendations": "Рекомендации приключений",
|
"adventure_recommendations": "Рекомендации приключений",
|
||||||
"food": "Еда",
|
"food": "Еда",
|
||||||
"tourism": "Туризм"
|
"tourism": "Туризм"
|
||||||
|
},
|
||||||
|
"calendar": {
|
||||||
|
"all_categories": "Все категории",
|
||||||
|
"all_day_event": "Событие на весь день",
|
||||||
|
"calendar_overview": "Обзор календаря",
|
||||||
|
"categories": "Категории",
|
||||||
|
"day": "День",
|
||||||
|
"events_scheduled": "События запланированы",
|
||||||
|
"filter_by_category": "Фильтр по категории",
|
||||||
|
"filtered_results": "Отфильтрованные результаты",
|
||||||
|
"month": "Месяц",
|
||||||
|
"today": "Сегодня",
|
||||||
|
"total_events": "Общее количество событий",
|
||||||
|
"week": "Неделя"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -243,7 +243,10 @@
|
||||||
"delete_collection_warning": "Är du säker på att du vill ta bort den här samlingen? \nDenna åtgärd kan inte ångras.",
|
"delete_collection_warning": "Är du säker på att du vill ta bort den här samlingen? \nDenna åtgärd kan inte ångras.",
|
||||||
"done": "Gjort",
|
"done": "Gjort",
|
||||||
"loading_adventures": "Laddar äventyr ...",
|
"loading_adventures": "Laddar äventyr ...",
|
||||||
"name_location": "namn, plats"
|
"name_location": "namn, plats",
|
||||||
|
"collection_contents": "Insamlingsinnehåll",
|
||||||
|
"check_in": "Checka in",
|
||||||
|
"check_out": "Checka ut"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"desc_1": "Upptäck, planera och utforska med lätthet",
|
"desc_1": "Upptäck, planera och utforska med lätthet",
|
||||||
|
@ -346,7 +349,12 @@
|
||||||
"show_map_labels": "Visa kartetiketter",
|
"show_map_labels": "Visa kartetiketter",
|
||||||
"total_cities": "Totala städer",
|
"total_cities": "Totala städer",
|
||||||
"total_countries": "Totala länder",
|
"total_countries": "Totala länder",
|
||||||
"total_regions": "Totala regioner"
|
"total_regions": "Totala regioner",
|
||||||
|
"newest_first": "Nyaste första",
|
||||||
|
"oldest_first": "Äldsta först",
|
||||||
|
"unvisited_first": "Oöverträffad först",
|
||||||
|
"visited_first": "Besökt först",
|
||||||
|
"total_items": "Totala artiklar"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"confirm_password": "Bekräfta lösenord",
|
"confirm_password": "Bekräfta lösenord",
|
||||||
|
@ -685,5 +693,19 @@
|
||||||
},
|
},
|
||||||
"google_maps": {
|
"google_maps": {
|
||||||
"google_maps_integration_desc": "Anslut ditt Google Maps-konto för att få sökresultat och rekommendationer av hög kvalitet."
|
"google_maps_integration_desc": "Anslut ditt Google Maps-konto för att få sökresultat och rekommendationer av hög kvalitet."
|
||||||
|
},
|
||||||
|
"calendar": {
|
||||||
|
"all_categories": "Alla kategorier",
|
||||||
|
"all_day_event": "Hela dagen",
|
||||||
|
"calendar_overview": "Kalenderöversikt",
|
||||||
|
"categories": "Kategorier",
|
||||||
|
"day": "Dag",
|
||||||
|
"events_scheduled": "Händelser planerade",
|
||||||
|
"filter_by_category": "Filter efter kategori",
|
||||||
|
"filtered_results": "Filtrerade resultat",
|
||||||
|
"month": "Månad",
|
||||||
|
"today": "I dag",
|
||||||
|
"total_events": "Totala evenemang",
|
||||||
|
"week": "Vecka"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -295,7 +295,10 @@
|
||||||
"delete_collection_warning": "您确定要删除此系列吗?\n该动作不能撤消。",
|
"delete_collection_warning": "您确定要删除此系列吗?\n该动作不能撤消。",
|
||||||
"done": "完毕",
|
"done": "完毕",
|
||||||
"loading_adventures": "加载冒险...",
|
"loading_adventures": "加载冒险...",
|
||||||
"name_location": "名称,位置"
|
"name_location": "名称,位置",
|
||||||
|
"collection_contents": "收集内容",
|
||||||
|
"check_in": "报到",
|
||||||
|
"check_out": "查看"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"forgot_password": "忘记密码?",
|
"forgot_password": "忘记密码?",
|
||||||
|
@ -368,7 +371,12 @@
|
||||||
"show_map": "显示地图",
|
"show_map": "显示地图",
|
||||||
"show_map_labels": "显示地图标签",
|
"show_map_labels": "显示地图标签",
|
||||||
"total_cities": "总城市",
|
"total_cities": "总城市",
|
||||||
"total_countries": "总国家"
|
"total_countries": "总国家",
|
||||||
|
"newest_first": "最新的第一",
|
||||||
|
"oldest_first": "最古老的第一",
|
||||||
|
"unvisited_first": "首先未访问",
|
||||||
|
"visited_first": "首先访问",
|
||||||
|
"total_items": "总项目"
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
"no_users_found": "未找到已公开个人资料的用户。"
|
"no_users_found": "未找到已公开个人资料的用户。"
|
||||||
|
@ -685,5 +693,19 @@
|
||||||
},
|
},
|
||||||
"google_maps": {
|
"google_maps": {
|
||||||
"google_maps_integration_desc": "连接您的Google Maps帐户以获取高质量的位置搜索结果和建议。"
|
"google_maps_integration_desc": "连接您的Google Maps帐户以获取高质量的位置搜索结果和建议。"
|
||||||
|
},
|
||||||
|
"calendar": {
|
||||||
|
"all_categories": "所有类别",
|
||||||
|
"all_day_event": "全天活动",
|
||||||
|
"calendar_overview": "日历概述",
|
||||||
|
"categories": "类别",
|
||||||
|
"day": "天",
|
||||||
|
"events_scheduled": "预定事件",
|
||||||
|
"filter_by_category": "按类别过滤",
|
||||||
|
"filtered_results": "过滤结果",
|
||||||
|
"month": "月",
|
||||||
|
"today": "今天",
|
||||||
|
"total_events": "总事件",
|
||||||
|
"week": "星期"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,7 +85,7 @@ async function handleRequest(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const responseData = await response.text();
|
const responseData = await response.bytes();
|
||||||
// Create a new Headers object without the 'set-cookie' header
|
// Create a new Headers object without the 'set-cookie' header
|
||||||
const cleanHeaders = new Headers(response.headers);
|
const cleanHeaders = new Headers(response.headers);
|
||||||
cleanHeaders.delete('set-cookie');
|
cleanHeaders.delete('set-cookie');
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import type { Adventure } from '$lib/types';
|
import type { Adventure } from '$lib/types';
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
|
import { formatDateInTimezone, formatAllDayDate } from '$lib/dateUtils';
|
||||||
|
import { isAllDay } from '$lib';
|
||||||
|
|
||||||
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
||||||
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||||
|
|
||||||
|
@ -12,21 +15,114 @@ export const load = (async (event) => {
|
||||||
});
|
});
|
||||||
let adventures = (await visitedFetch.json()) as Adventure[];
|
let adventures = (await visitedFetch.json()) as Adventure[];
|
||||||
|
|
||||||
|
// Get user's local timezone as fallback
|
||||||
|
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
|
||||||
let dates: Array<{
|
let dates: Array<{
|
||||||
id: string;
|
id: string;
|
||||||
start: string;
|
start: string;
|
||||||
end: string;
|
end: string;
|
||||||
title: string;
|
title: string;
|
||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
|
extendedProps?: {
|
||||||
|
adventureName: string;
|
||||||
|
category: string;
|
||||||
|
icon: string;
|
||||||
|
timezone: string;
|
||||||
|
isAllDay: boolean;
|
||||||
|
formattedStart: string;
|
||||||
|
formattedEnd: string;
|
||||||
|
location?: string;
|
||||||
|
description?: string;
|
||||||
|
adventureId?: string;
|
||||||
|
};
|
||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
adventures.forEach((adventure) => {
|
adventures.forEach((adventure) => {
|
||||||
adventure.visits.forEach((visit) => {
|
adventure.visits.forEach((visit) => {
|
||||||
if (visit.start_date) {
|
if (visit.start_date) {
|
||||||
|
let startDate = visit.start_date;
|
||||||
|
let endDate = visit.end_date || visit.start_date;
|
||||||
|
const targetTimezone = visit.timezone || userTimezone;
|
||||||
|
const allDay = isAllDay(visit.start_date);
|
||||||
|
|
||||||
|
// Handle timezone conversion for non-all-day events
|
||||||
|
if (!allDay) {
|
||||||
|
// Convert UTC dates to target timezone
|
||||||
|
const startDateTime = new Date(visit.start_date);
|
||||||
|
const endDateTime = new Date(visit.end_date || visit.start_date);
|
||||||
|
|
||||||
|
// Format for calendar (ISO string in target timezone)
|
||||||
|
startDate = new Intl.DateTimeFormat('sv-SE', {
|
||||||
|
timeZone: targetTimezone,
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hourCycle: 'h23'
|
||||||
|
})
|
||||||
|
.format(startDateTime)
|
||||||
|
.replace(' ', 'T');
|
||||||
|
|
||||||
|
endDate = new Intl.DateTimeFormat('sv-SE', {
|
||||||
|
timeZone: targetTimezone,
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hourCycle: 'h23'
|
||||||
|
})
|
||||||
|
.format(endDateTime)
|
||||||
|
.replace(' ', 'T');
|
||||||
|
} else {
|
||||||
|
// For all-day events, use just the date part
|
||||||
|
startDate = visit.start_date.split('T')[0];
|
||||||
|
|
||||||
|
// For all-day events, add one day to end date to make it inclusive
|
||||||
|
const endDateObj = new Date(visit.end_date || visit.start_date);
|
||||||
|
endDateObj.setDate(endDateObj.getDate() + 1);
|
||||||
|
endDate = endDateObj.toISOString().split('T')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create detailed title with timezone info
|
||||||
|
let detailedTitle = adventure.name;
|
||||||
|
if (adventure.category?.icon) {
|
||||||
|
detailedTitle = `${adventure.category.icon} ${detailedTitle}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add time info to title for non-all-day events
|
||||||
|
if (!allDay) {
|
||||||
|
const startTime = formatDateInTimezone(visit.start_date, targetTimezone);
|
||||||
|
detailedTitle += ` (${startTime.split(' ').slice(-2).join(' ')})`;
|
||||||
|
if (targetTimezone !== userTimezone) {
|
||||||
|
detailedTitle += ` ${targetTimezone}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dates.push({
|
dates.push({
|
||||||
id: adventure.id,
|
id: adventure.id,
|
||||||
start: visit.start_date,
|
start: startDate,
|
||||||
end: visit.end_date || visit.start_date,
|
end: endDate,
|
||||||
title: adventure.name + (adventure.category?.icon ? ' ' + adventure.category.icon : '')
|
title: detailedTitle,
|
||||||
|
backgroundColor: '#3b82f6',
|
||||||
|
extendedProps: {
|
||||||
|
adventureName: adventure.name,
|
||||||
|
category: adventure.category?.name || 'Adventure',
|
||||||
|
icon: adventure.category?.icon || '🗺️',
|
||||||
|
timezone: targetTimezone,
|
||||||
|
isAllDay: allDay,
|
||||||
|
formattedStart: allDay
|
||||||
|
? formatAllDayDate(visit.start_date)
|
||||||
|
: formatDateInTimezone(visit.start_date, targetTimezone),
|
||||||
|
formattedEnd: allDay
|
||||||
|
? formatAllDayDate(visit.end_date || visit.start_date)
|
||||||
|
: formatDateInTimezone(visit.end_date || visit.start_date, targetTimezone),
|
||||||
|
location: adventure.location || '',
|
||||||
|
description: adventure.description || '',
|
||||||
|
adventureId: adventure.id
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,38 +1,437 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import Calendar from '@event-calendar/core';
|
import Calendar from '@event-calendar/core';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import TimeGrid from '@event-calendar/time-grid';
|
import TimeGrid from '@event-calendar/time-grid';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import DayGrid from '@event-calendar/day-grid';
|
import DayGrid from '@event-calendar/day-grid';
|
||||||
|
// @ts-ignore
|
||||||
|
import Interaction from '@event-calendar/interaction';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import CalendarIcon from '~icons/mdi/calendar';
|
||||||
|
import DownloadIcon from '~icons/mdi/download';
|
||||||
|
import FilterIcon from '~icons/mdi/filter-variant';
|
||||||
|
import CloseIcon from '~icons/mdi/close';
|
||||||
|
import MapMarkerIcon from '~icons/mdi/map-marker';
|
||||||
|
import ClockIcon from '~icons/mdi/clock';
|
||||||
|
import SearchIcon from '~icons/mdi/magnify';
|
||||||
|
import ClearIcon from '~icons/mdi/close';
|
||||||
|
import { marked } from 'marked'; // Import the markdown parser
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
|
||||||
|
const renderMarkdown = (markdown: string) => {
|
||||||
|
return marked(markdown);
|
||||||
|
};
|
||||||
|
|
||||||
let adventures = data.props.adventures;
|
let adventures = data.props.adventures;
|
||||||
let dates = data.props.dates;
|
let allDates = data.props.dates;
|
||||||
|
let filteredDates = [...allDates];
|
||||||
|
|
||||||
let icsCalendar = data.props.ics_calendar;
|
let icsCalendar = data.props.ics_calendar;
|
||||||
// turn the ics calendar into a data URL
|
|
||||||
let icsCalendarDataUrl = URL.createObjectURL(new Blob([icsCalendar], { type: 'text/calendar' }));
|
let icsCalendarDataUrl = URL.createObjectURL(new Blob([icsCalendar], { type: 'text/calendar' }));
|
||||||
|
|
||||||
let plugins = [TimeGrid, DayGrid];
|
// Modal state
|
||||||
let options = {
|
let selectedEvent: any = null;
|
||||||
|
let showEventModal = false;
|
||||||
|
|
||||||
|
// Filter state
|
||||||
|
let showFilters = false;
|
||||||
|
let searchFilter = '';
|
||||||
|
let sidebarOpen = false;
|
||||||
|
|
||||||
|
// Get unique categories for filter
|
||||||
|
|
||||||
|
// Apply filters
|
||||||
|
$: {
|
||||||
|
filteredDates = allDates.filter((event) => {
|
||||||
|
const matchesSearch =
|
||||||
|
!searchFilter ||
|
||||||
|
event.extendedProps?.adventureName.toLowerCase().includes(searchFilter.toLowerCase()) ||
|
||||||
|
event.extendedProps?.location?.toLowerCase().includes(searchFilter.toLowerCase());
|
||||||
|
|
||||||
|
return matchesSearch;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let plugins = [TimeGrid, DayGrid, Interaction];
|
||||||
|
$: options = {
|
||||||
view: 'dayGridMonth',
|
view: 'dayGridMonth',
|
||||||
events: [...dates]
|
events: filteredDates,
|
||||||
|
headerToolbar: {
|
||||||
|
start: 'prev,next today',
|
||||||
|
center: 'title',
|
||||||
|
end: 'dayGridMonth,timeGridWeek,timeGridDay'
|
||||||
|
},
|
||||||
|
buttonText: {
|
||||||
|
today: $t('calendar.today'),
|
||||||
|
dayGridMonth: $t('calendar.month'),
|
||||||
|
timeGridWeek: $t('calendar.week'),
|
||||||
|
timeGridDay: $t('calendar.day')
|
||||||
|
},
|
||||||
|
height: 'auto',
|
||||||
|
eventDisplay: 'block',
|
||||||
|
dayMaxEvents: 3,
|
||||||
|
moreLinkText: (num: number) => `+${num} more`,
|
||||||
|
eventClick: (info: any) => {
|
||||||
|
selectedEvent = info.event;
|
||||||
|
showEventModal = true;
|
||||||
|
},
|
||||||
|
eventMouseEnter: (info: any) => {
|
||||||
|
info.el.style.cursor = 'pointer';
|
||||||
|
},
|
||||||
|
themeSystem: 'standard'
|
||||||
};
|
};
|
||||||
console.log(dates);
|
|
||||||
|
function clearFilters() {
|
||||||
|
searchFilter = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
showEventModal = false;
|
||||||
|
selectedEvent = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSidebar() {
|
||||||
|
sidebarOpen = !sidebarOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
// Add custom CSS for calendar styling
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
.ec-toolbar {
|
||||||
|
background: hsl(var(--b2)) !important;
|
||||||
|
border-radius: 0.75rem !important;
|
||||||
|
padding: 1.25rem !important;
|
||||||
|
margin-bottom: 1.5rem !important;
|
||||||
|
border: 1px solid hsl(var(--b3)) !important;
|
||||||
|
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1) !important;
|
||||||
|
}
|
||||||
|
.ec-button {
|
||||||
|
background: hsl(var(--b3)) !important;
|
||||||
|
border: 1px solid hsl(var(--b3)) !important;
|
||||||
|
color: hsl(var(--bc)) !important;
|
||||||
|
border-radius: 0.5rem !important;
|
||||||
|
padding: 0.5rem 1rem !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
transition: all 0.2s ease !important;
|
||||||
|
}
|
||||||
|
.ec-button:hover {
|
||||||
|
background: hsl(var(--b1)) !important;
|
||||||
|
transform: translateY(-1px) !important;
|
||||||
|
box-shadow: 0 4px 12px rgb(0 0 0 / 0.15) !important;
|
||||||
|
}
|
||||||
|
.ec-button.ec-button-active {
|
||||||
|
background: hsl(var(--p)) !important;
|
||||||
|
color: hsl(var(--pc)) !important;
|
||||||
|
box-shadow: 0 4px 12px hsl(var(--p) / 0.3) !important;
|
||||||
|
}
|
||||||
|
.ec-day {
|
||||||
|
background: hsl(var(--b1)) !important;
|
||||||
|
border: 1px solid hsl(var(--b3)) !important;
|
||||||
|
transition: background-color 0.2s ease !important;
|
||||||
|
}
|
||||||
|
.ec-day:hover {
|
||||||
|
background: hsl(var(--b2)) !important;
|
||||||
|
}
|
||||||
|
.ec-day-today {
|
||||||
|
background: hsl(var(--b2)) !important;
|
||||||
|
position: relative !important;
|
||||||
|
}
|
||||||
|
.ec-day-today::before {
|
||||||
|
content: '' !important;
|
||||||
|
position: absolute !important;
|
||||||
|
top: 0 !important;
|
||||||
|
left: 0 !important;
|
||||||
|
right: 0 !important;
|
||||||
|
height: 3px !important;
|
||||||
|
background: hsl(var(--p)) !important;
|
||||||
|
border-radius: 0.25rem !important;
|
||||||
|
}
|
||||||
|
.ec-event {
|
||||||
|
border-radius: 0.375rem !important;
|
||||||
|
padding: 0.25rem 0.5rem !important;
|
||||||
|
font-size: 0.75rem !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
transition: all 0.2s ease !important;
|
||||||
|
box-shadow: 0 1px 3px rgb(0 0 0 / 0.1) !important;
|
||||||
|
}
|
||||||
|
.ec-event:hover {
|
||||||
|
transform: translateY(-1px) !important;
|
||||||
|
box-shadow: 0 4px 12px rgb(0 0 0 / 0.15) !important;
|
||||||
|
}
|
||||||
|
.ec-view {
|
||||||
|
background: hsl(var(--b1)) !important;
|
||||||
|
border-radius: 0.75rem !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1 class="text-center text-2xl font-bold">{$t('adventures.adventure_calendar')}</h1>
|
<svelte:head>
|
||||||
|
<title>{$t('adventures.adventure_calendar')} - AdventureLog</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
<Calendar {plugins} {options} />
|
<div class="min-h-screen bg-gradient-to-br from-base-200 via-base-100 to-base-200">
|
||||||
|
<div class="drawer lg:drawer-open">
|
||||||
|
<input id="calendar-drawer" type="checkbox" class="drawer-toggle" bind:checked={sidebarOpen} />
|
||||||
|
|
||||||
<!-- download calendar -->
|
<div class="drawer-content">
|
||||||
<div class="flex items-center justify-center mt-4">
|
<!-- Header Section -->
|
||||||
<a href={icsCalendarDataUrl} download="adventures.ics" class="btn btn-primary"
|
<div class="sticky top-0 z-40 bg-base-100/80 backdrop-blur-lg border-b border-base-300">
|
||||||
>{$t('adventures.download_calendar')}</a
|
<div class="container mx-auto px-6 py-4">
|
||||||
>
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<button class="btn btn-ghost btn-square lg:hidden" on:click={toggleSidebar}>
|
||||||
|
<FilterIcon class="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="p-2 bg-primary/10 rounded-xl">
|
||||||
|
<CalendarIcon class="w-8 h-8 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold text-primary bg-clip-text">
|
||||||
|
{$t('adventures.adventure_calendar')}
|
||||||
|
</h1>
|
||||||
|
<p class="text-sm text-base-content/60">
|
||||||
|
{filteredDates.length}
|
||||||
|
{$t('calendar.events_scheduled')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Stats -->
|
||||||
|
<div class="hidden md:flex items-center gap-2">
|
||||||
|
<div class="stats stats-horizontal bg-base-200/50 border border-base-300/50">
|
||||||
|
<div class="stat py-2 px-4">
|
||||||
|
<div class="stat-title text-xs">{$t('calendar.total_events')}</div>
|
||||||
|
<div class="stat-value text-lg text-primary">{allDates.length}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat py-2 px-4">
|
||||||
|
<div class="stat-title text-xs">{$t('navbar.adventures')}</div>
|
||||||
|
<div class="stat-value text-lg text-secondary">{adventures.length}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search Bar -->
|
||||||
|
<div class="mt-4 flex items-center gap-4">
|
||||||
|
<div class="relative flex-1 max-w-md">
|
||||||
|
<SearchIcon
|
||||||
|
class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-base-content/40"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search adventures or locations..."
|
||||||
|
class="input input-bordered w-full pl-10 pr-10 bg-base-100/80"
|
||||||
|
bind:value={searchFilter}
|
||||||
|
/>
|
||||||
|
{#if searchFilter.length > 0}
|
||||||
|
<button
|
||||||
|
class="absolute right-3 top-1/2 -translate-y-1/2 text-base-content/40 hover:text-base-content"
|
||||||
|
on:click={() => (searchFilter = '')}
|
||||||
|
>
|
||||||
|
<ClearIcon class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter Chips -->
|
||||||
|
<div class="mt-4 flex flex-wrap items-center gap-2">
|
||||||
|
<span class="text-sm font-medium text-base-content/60"
|
||||||
|
>{$t('worldtravel.filter_by')}:</span
|
||||||
|
>
|
||||||
|
{#if searchFilter}
|
||||||
|
<button class="btn btn-ghost btn-xs gap-1" on:click={clearFilters}>
|
||||||
|
<ClearIcon class="w-3 h-3" />
|
||||||
|
{$t('worldtravel.clear_all')}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="container mx-auto px-6 py-8">
|
||||||
|
<!-- Calendar -->
|
||||||
|
<div class="card bg-base-100 shadow-2xl border border-base-300/50">
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<Calendar {plugins} {options} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="drawer-side z-50">
|
||||||
|
<label for="calendar-drawer" class="drawer-overlay"></label>
|
||||||
|
<div class="w-80 min-h-full bg-base-100 shadow-2xl">
|
||||||
|
<div class="p-6">
|
||||||
|
<!-- Sidebar Header -->
|
||||||
|
<div class="flex items-center gap-3 mb-8">
|
||||||
|
<div class="p-2 bg-primary/10 rounded-lg">
|
||||||
|
<FilterIcon class="w-6 h-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<h2 class="text-xl font-bold">{$t('adventures.filters_and_stats')}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Calendar Statistics -->
|
||||||
|
<div class="card bg-base-200/50 p-4 mb-6">
|
||||||
|
<h3 class="font-semibold text-lg mb-4 flex items-center gap-2">
|
||||||
|
<CalendarIcon class="w-5 h-5" />
|
||||||
|
{$t('calendar.calendar_overview')}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="stat p-0">
|
||||||
|
<div class="stat-title text-sm">{$t('calendar.total_events')}</div>
|
||||||
|
<div class="stat-value text-2xl">{allDates.length}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="stat p-0">
|
||||||
|
<div class="stat-title text-xs">{$t('navbar.adventures')}</div>
|
||||||
|
<div class="stat-value text-lg text-primary">{adventures.length}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if filteredDates.length !== allDates.length}
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex justify-between text-sm">
|
||||||
|
<span>{$t('calendar.filtered_results')}</span>
|
||||||
|
<span>{filteredDates.length} {$t('worldtravel.of')} {allDates.length}</span>
|
||||||
|
</div>
|
||||||
|
<progress
|
||||||
|
class="progress progress-primary w-full"
|
||||||
|
value={filteredDates.length}
|
||||||
|
max={allDates.length}
|
||||||
|
></progress>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<div class="space-y-3">
|
||||||
|
<a
|
||||||
|
href={icsCalendarDataUrl}
|
||||||
|
download="adventures.ics"
|
||||||
|
class="btn btn-primary w-full gap-2"
|
||||||
|
>
|
||||||
|
<DownloadIcon class="w-4 h-4" />
|
||||||
|
{$t('adventures.download_calendar')}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<button class="btn btn-ghost w-full gap-2" on:click={clearFilters}>
|
||||||
|
<ClearIcon class="w-4 h-4" />
|
||||||
|
{$t('worldtravel.clear_filters')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Event Details Modal -->
|
||||||
|
{#if showEventModal && selectedEvent}
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<div class="modal modal-open">
|
||||||
|
<div class="modal-box max-w-2xl bg-base-100 border border-base-300/50 shadow-2xl">
|
||||||
|
<div class="flex items-start justify-between mb-6">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="p-3 bg-primary/10 rounded-xl">
|
||||||
|
<span class="text-2xl">{selectedEvent.extendedProps.icon}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-2xl font-bold">{selectedEvent.extendedProps.adventureName}</h3>
|
||||||
|
<div class="badge badge-primary badge-lg mt-2">
|
||||||
|
{selectedEvent.extendedProps.category}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-ghost btn-sm btn-circle" on:click={closeModal}>
|
||||||
|
<CloseIcon class="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<!-- Date & Time -->
|
||||||
|
<div class="card bg-base-200/50 border border-base-300/30">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<ClockIcon class="w-6 h-6 text-primary flex-shrink-0" />
|
||||||
|
<div>
|
||||||
|
<div class="font-semibold text-lg">
|
||||||
|
{#if selectedEvent.extendedProps.isAllDay}
|
||||||
|
{$t('calendar.all_day_event')}
|
||||||
|
{:else}
|
||||||
|
{selectedEvent.extendedProps.formattedStart}
|
||||||
|
{#if selectedEvent.extendedProps.formattedEnd !== selectedEvent.extendedProps.formattedStart}
|
||||||
|
→ {selectedEvent.extendedProps.formattedEnd}
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if !selectedEvent.extendedProps.isAllDay && selectedEvent.extendedProps.timezone}
|
||||||
|
<div class="text-sm text-base-content/70 mt-1">
|
||||||
|
{selectedEvent.extendedProps.timezone}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Location -->
|
||||||
|
{#if selectedEvent.extendedProps.location}
|
||||||
|
<div class="card bg-base-200/50 border border-base-300/30">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<MapMarkerIcon class="w-6 h-6 text-primary flex-shrink-0" />
|
||||||
|
<div class="font-semibold text-lg">{selectedEvent.extendedProps.location}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
{#if selectedEvent.extendedProps.description}
|
||||||
|
<div class="card bg-base-200/50 border border-base-300/30">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="font-semibold text-lg mb-3">{$t('adventures.description')}</div>
|
||||||
|
<article
|
||||||
|
class="prose overflow-auto h-full max-w-full p-4 border border-base-300 rounded-lg mb-4 mt-4"
|
||||||
|
>
|
||||||
|
{@html renderMarkdown(selectedEvent.extendedProps.description || '')}
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if selectedEvent.extendedProps.adventureId}
|
||||||
|
<a
|
||||||
|
href={`/adventures/${selectedEvent.extendedProps.adventureId}`}
|
||||||
|
class="btn btn-neutral btn-block mt-4"
|
||||||
|
>
|
||||||
|
{$t('map.view_details')}
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-action mt-8">
|
||||||
|
<button class="btn btn-primary btn-lg" on:click={closeModal}> {$t('about.close')} </button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
<div class="modal-backdrop" on:click={closeModal}></div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
|
@ -17,12 +17,13 @@
|
||||||
import Plus from '~icons/mdi/plus';
|
import Plus from '~icons/mdi/plus';
|
||||||
import AdventureCard from '$lib/components/AdventureCard.svelte';
|
import AdventureCard from '$lib/components/AdventureCard.svelte';
|
||||||
import AdventureLink from '$lib/components/AdventureLink.svelte';
|
import AdventureLink from '$lib/components/AdventureLink.svelte';
|
||||||
import NotFound from '$lib/components/NotFound.svelte';
|
import { MapLibre, Marker, Popup, LineLayer, GeoJSON } from 'svelte-maplibre';
|
||||||
import { DefaultMarker, MapLibre, Marker, Popup, LineLayer, GeoJSON } from 'svelte-maplibre';
|
|
||||||
import TransportationCard from '$lib/components/TransportationCard.svelte';
|
import TransportationCard from '$lib/components/TransportationCard.svelte';
|
||||||
import NoteCard from '$lib/components/NoteCard.svelte';
|
import NoteCard from '$lib/components/NoteCard.svelte';
|
||||||
import NoteModal from '$lib/components/NoteModal.svelte';
|
import NoteModal from '$lib/components/NoteModal.svelte';
|
||||||
|
|
||||||
|
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
|
||||||
import {
|
import {
|
||||||
groupAdventuresByDate,
|
groupAdventuresByDate,
|
||||||
groupNotesByDate,
|
groupNotesByDate,
|
||||||
|
@ -31,8 +32,11 @@
|
||||||
osmTagToEmoji,
|
osmTagToEmoji,
|
||||||
groupLodgingByDate,
|
groupLodgingByDate,
|
||||||
LODGING_TYPES_ICONS,
|
LODGING_TYPES_ICONS,
|
||||||
getBasemapUrl
|
getBasemapUrl,
|
||||||
|
isAllDay
|
||||||
} from '$lib';
|
} from '$lib';
|
||||||
|
import { formatDateInTimezone, formatAllDayDate } from '$lib/dateUtils';
|
||||||
|
|
||||||
import ChecklistCard from '$lib/components/ChecklistCard.svelte';
|
import ChecklistCard from '$lib/components/ChecklistCard.svelte';
|
||||||
import ChecklistModal from '$lib/components/ChecklistModal.svelte';
|
import ChecklistModal from '$lib/components/ChecklistModal.svelte';
|
||||||
import AdventureModal from '$lib/components/AdventureModal.svelte';
|
import AdventureModal from '$lib/components/AdventureModal.svelte';
|
||||||
|
@ -41,6 +45,7 @@
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import LodgingModal from '$lib/components/LodgingModal.svelte';
|
import LodgingModal from '$lib/components/LodgingModal.svelte';
|
||||||
import LodgingCard from '$lib/components/LodgingCard.svelte';
|
import LodgingCard from '$lib/components/LodgingCard.svelte';
|
||||||
|
import CollectionAllView from '$lib/components/CollectionAllView.svelte';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
console.log(data);
|
console.log(data);
|
||||||
|
@ -59,19 +64,6 @@
|
||||||
|
|
||||||
let collection: Collection;
|
let collection: Collection;
|
||||||
|
|
||||||
// add christmas and new years
|
|
||||||
// dates = Array.from({ length: 25 }, (_, i) => {
|
|
||||||
// const date = new Date();
|
|
||||||
// date.setMonth(11);
|
|
||||||
// date.setDate(i + 1);
|
|
||||||
// return {
|
|
||||||
// id: i.toString(),
|
|
||||||
// start: date.toISOString(),
|
|
||||||
// end: date.toISOString(),
|
|
||||||
// title: '🎄'
|
|
||||||
// };
|
|
||||||
// });
|
|
||||||
|
|
||||||
let dates: Array<{
|
let dates: Array<{
|
||||||
id: string;
|
id: string;
|
||||||
start: string;
|
start: string;
|
||||||
|
@ -92,16 +84,79 @@
|
||||||
dates = [];
|
dates = [];
|
||||||
|
|
||||||
if (adventures) {
|
if (adventures) {
|
||||||
dates = dates.concat(
|
adventures.forEach((adventure) => {
|
||||||
adventures.flatMap((adventure) =>
|
adventure.visits.forEach((visit) => {
|
||||||
adventure.visits.map((visit) => ({
|
if (visit.start_date) {
|
||||||
|
let startDate = visit.start_date;
|
||||||
|
let endDate = visit.end_date || visit.start_date;
|
||||||
|
const targetTimezone = visit.timezone || userTimezone;
|
||||||
|
const allDay = isAllDay(visit.start_date);
|
||||||
|
|
||||||
|
// Handle timezone conversion for non-all-day events
|
||||||
|
if (!allDay) {
|
||||||
|
// Convert UTC dates to target timezone
|
||||||
|
const startDateTime = new Date(visit.start_date);
|
||||||
|
const endDateTime = new Date(visit.end_date || visit.start_date);
|
||||||
|
|
||||||
|
// Format for calendar (ISO string in target timezone)
|
||||||
|
startDate = new Intl.DateTimeFormat('sv-SE', {
|
||||||
|
timeZone: targetTimezone,
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hourCycle: 'h23'
|
||||||
|
})
|
||||||
|
.format(startDateTime)
|
||||||
|
.replace(' ', 'T');
|
||||||
|
|
||||||
|
endDate = new Intl.DateTimeFormat('sv-SE', {
|
||||||
|
timeZone: targetTimezone,
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hourCycle: 'h23'
|
||||||
|
})
|
||||||
|
.format(endDateTime)
|
||||||
|
.replace(' ', 'T');
|
||||||
|
} else {
|
||||||
|
// For all-day events, use just the date part
|
||||||
|
startDate = visit.start_date.split('T')[0];
|
||||||
|
|
||||||
|
// For all-day events, add one day to end date to make it inclusive
|
||||||
|
const endDateObj = new Date(visit.end_date || visit.start_date);
|
||||||
|
endDateObj.setDate(endDateObj.getDate() + 1);
|
||||||
|
endDate = endDateObj.toISOString().split('T')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create detailed title with timezone info
|
||||||
|
let detailedTitle = adventure.name;
|
||||||
|
if (adventure.category?.icon) {
|
||||||
|
detailedTitle = `${adventure.category.icon} ${detailedTitle}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add time info to title for non-all-day events
|
||||||
|
if (!allDay) {
|
||||||
|
const startTime = formatDateInTimezone(visit.start_date, targetTimezone);
|
||||||
|
detailedTitle += ` (${startTime.split(' ').slice(-2).join(' ')})`;
|
||||||
|
if (targetTimezone !== userTimezone) {
|
||||||
|
detailedTitle += ` ${targetTimezone}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dates.push({
|
||||||
id: adventure.id,
|
id: adventure.id,
|
||||||
start: visit.start_date || '', // Ensure it's a string
|
start: startDate,
|
||||||
end: visit.end_date || visit.start_date || '', // Ensure it's a string
|
end: endDate,
|
||||||
title: adventure.name + (adventure.category?.icon ? ' ' + adventure.category.icon : '')
|
title: detailedTitle,
|
||||||
}))
|
backgroundColor: '#3b82f6'
|
||||||
)
|
});
|
||||||
);
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (transportations) {
|
if (transportations) {
|
||||||
|
@ -112,7 +167,8 @@
|
||||||
id: transportation.id,
|
id: transportation.id,
|
||||||
start: transportation.date || '', // Ensure it's a string
|
start: transportation.date || '', // Ensure it's a string
|
||||||
end: transportation.end_date || transportation.date || '', // Ensure it's a string
|
end: transportation.end_date || transportation.date || '', // Ensure it's a string
|
||||||
title: transportation.name + (transportation.type ? ` (${transportation.type})` : '')
|
title: transportation.name + (transportation.type ? ` (${transportation.type})` : ''),
|
||||||
|
backgroundColor: '#10b981'
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -121,12 +177,61 @@
|
||||||
dates = dates.concat(
|
dates = dates.concat(
|
||||||
lodging
|
lodging
|
||||||
.filter((i) => i.check_in)
|
.filter((i) => i.check_in)
|
||||||
.map((lodging) => ({
|
.map((lodging) => {
|
||||||
|
const checkIn = lodging.check_in;
|
||||||
|
const checkOut = lodging.check_out || lodging.check_in;
|
||||||
|
if (!checkIn) return null;
|
||||||
|
|
||||||
|
const isAlldayLodging: boolean = isAllDay(checkIn as string);
|
||||||
|
|
||||||
|
let startDate: string;
|
||||||
|
let endDate: string;
|
||||||
|
|
||||||
|
if (isAlldayLodging) {
|
||||||
|
// For all-day, use date part only, no timezone conversion
|
||||||
|
startDate = (checkIn as string).split('T')[0];
|
||||||
|
|
||||||
|
const endDateObj = new Date(checkOut as string);
|
||||||
|
endDateObj.setDate(endDateObj.getDate());
|
||||||
|
endDate = endDateObj.toISOString().split('T')[0];
|
||||||
|
|
||||||
|
return {
|
||||||
id: lodging.id,
|
id: lodging.id,
|
||||||
start: lodging.check_in || '', // Ensure it's a string
|
start: startDate,
|
||||||
end: lodging.check_out || lodging.check_in || '', // Ensure it's a string
|
end: endDate,
|
||||||
title: lodging.name
|
title: `${getLodgingIcon(lodging.type)} ${lodging.name}`,
|
||||||
}))
|
backgroundColor: '#f59e0b'
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Only use timezone if not all-day
|
||||||
|
const lodgingTimezone = lodging.timezone || userTimezone;
|
||||||
|
const checkInDateTime = new Date(checkIn as string);
|
||||||
|
const checkOutDateTime = new Date(checkOut as string);
|
||||||
|
|
||||||
|
startDate = new Intl.DateTimeFormat('sv-SE', {
|
||||||
|
timeZone: lodgingTimezone,
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit'
|
||||||
|
}).format(checkInDateTime);
|
||||||
|
|
||||||
|
endDate = new Intl.DateTimeFormat('sv-SE', {
|
||||||
|
timeZone: lodgingTimezone,
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit'
|
||||||
|
}).format(checkOutDateTime);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: lodging.id,
|
||||||
|
start: startDate,
|
||||||
|
end: endDate,
|
||||||
|
title: lodging.name,
|
||||||
|
backgroundColor: '#f59e0b'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((item) => item !== null)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,11 +244,6 @@
|
||||||
|
|
||||||
let adventures: Adventure[] = [];
|
let adventures: Adventure[] = [];
|
||||||
|
|
||||||
// Add this after your existing MapLibre markers
|
|
||||||
|
|
||||||
// Add this after your existing MapLibre markers
|
|
||||||
|
|
||||||
// Create line data from orderedItems
|
|
||||||
$: lineData = createLineData(orderedItems);
|
$: lineData = createLineData(orderedItems);
|
||||||
|
|
||||||
// Function to create GeoJSON line data from ordered items
|
// Function to create GeoJSON line data from ordered items
|
||||||
|
@ -651,7 +751,7 @@
|
||||||
{#if data.user && data.user.uuid && (data.user.uuid == collection.user_id || (collection.shared_with && collection.shared_with.includes(data.user.uuid))) && !collection.is_archived}
|
{#if data.user && data.user.uuid && (data.user.uuid == collection.user_id || (collection.shared_with && collection.shared_with.includes(data.user.uuid))) && !collection.is_archived}
|
||||||
<div class="fixed bottom-4 right-4 z-[999]">
|
<div class="fixed bottom-4 right-4 z-[999]">
|
||||||
<div class="flex flex-row items-center justify-center gap-4">
|
<div class="flex flex-row items-center justify-center gap-4">
|
||||||
<div class="dropdown dropdown-top dropdown-end">
|
<div class="dropdown dropdown-top dropdown-end z-[999]">
|
||||||
<div tabindex="0" role="button" class="btn m-1 size-16 btn-primary">
|
<div tabindex="0" role="button" class="btn m-1 size-16 btn-primary">
|
||||||
<Plus class="w-8 h-8" />
|
<Plus class="w-8 h-8" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -844,100 +944,39 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if currentView == 'all'}
|
{#if currentView == 'all'}
|
||||||
{#if adventures.length > 0}
|
<CollectionAllView
|
||||||
<h1 class="text-center font-bold text-4xl mt-4 mb-2">{$t('adventures.linked_adventures')}</h1>
|
{adventures}
|
||||||
|
{transportations}
|
||||||
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
|
{lodging}
|
||||||
{#each adventures as adventure}
|
{notes}
|
||||||
<AdventureCard
|
{checklists}
|
||||||
user={data.user}
|
user={data.user}
|
||||||
on:edit={editAdventure}
|
|
||||||
on:delete={deleteAdventure}
|
|
||||||
{adventure}
|
|
||||||
{collection}
|
{collection}
|
||||||
/>
|
on:editAdventure={editAdventure}
|
||||||
{/each}
|
on:deleteAdventure={deleteAdventure}
|
||||||
</div>
|
on:editTransportation={editTransportation}
|
||||||
{/if}
|
on:deleteTransportation={(event) => {
|
||||||
|
|
||||||
{#if transportations.length > 0}
|
|
||||||
<h1 class="text-center font-bold text-4xl mt-4 mb-4">{$t('adventures.transportations')}</h1>
|
|
||||||
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
|
|
||||||
{#each transportations as transportation}
|
|
||||||
<TransportationCard
|
|
||||||
{transportation}
|
|
||||||
user={data?.user}
|
|
||||||
on:delete={(event) => {
|
|
||||||
transportations = transportations.filter((t) => t.id != event.detail);
|
transportations = transportations.filter((t) => t.id != event.detail);
|
||||||
}}
|
}}
|
||||||
on:edit={editTransportation}
|
on:editLodging={editLodging}
|
||||||
{collection}
|
on:deleteLodging={(event) => {
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if lodging.length > 0}
|
|
||||||
<h1 class="text-center font-bold text-4xl mt-4 mb-4">{$t('adventures.lodging')}</h1>
|
|
||||||
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
|
|
||||||
{#each lodging as hotel}
|
|
||||||
<LodgingCard
|
|
||||||
lodging={hotel}
|
|
||||||
user={data?.user}
|
|
||||||
on:delete={(event) => {
|
|
||||||
lodging = lodging.filter((t) => t.id != event.detail);
|
lodging = lodging.filter((t) => t.id != event.detail);
|
||||||
}}
|
}}
|
||||||
on:edit={editLodging}
|
on:editNote={(event) => {
|
||||||
{collection}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if notes.length > 0}
|
|
||||||
<h1 class="text-center font-bold text-4xl mt-4 mb-4">{$t('adventures.notes')}</h1>
|
|
||||||
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
|
|
||||||
{#each notes as note}
|
|
||||||
<NoteCard
|
|
||||||
{note}
|
|
||||||
user={data.user || null}
|
|
||||||
on:edit={(event) => {
|
|
||||||
noteToEdit = event.detail;
|
noteToEdit = event.detail;
|
||||||
isNoteModalOpen = true;
|
isNoteModalOpen = true;
|
||||||
}}
|
}}
|
||||||
on:delete={(event) => {
|
on:deleteNote={(event) => {
|
||||||
notes = notes.filter((n) => n.id != event.detail);
|
notes = notes.filter((n) => n.id != event.detail);
|
||||||
}}
|
}}
|
||||||
{collection}
|
on:editChecklist={(event) => {
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if checklists.length > 0}
|
|
||||||
<h1 class="text-center font-bold text-4xl mt-4 mb-4">{$t('adventures.checklists')}</h1>
|
|
||||||
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
|
|
||||||
{#each checklists as checklist}
|
|
||||||
<ChecklistCard
|
|
||||||
{checklist}
|
|
||||||
user={data.user || null}
|
|
||||||
on:delete={(event) => {
|
|
||||||
checklists = checklists.filter((n) => n.id != event.detail);
|
|
||||||
}}
|
|
||||||
on:edit={(event) => {
|
|
||||||
checklistToEdit = event.detail;
|
checklistToEdit = event.detail;
|
||||||
isShowingChecklistModal = true;
|
isShowingChecklistModal = true;
|
||||||
}}
|
}}
|
||||||
{collection}
|
on:deleteChecklist={(event) => {
|
||||||
|
checklists = checklists.filter((n) => n.id != event.detail);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- if none found -->
|
|
||||||
{#if adventures.length == 0 && transportations.length == 0 && notes.length == 0 && checklists.length == 0 && lodging.length == 0}
|
|
||||||
<NotFound error={undefined} />
|
|
||||||
{/if}
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if collection.start_date && collection.end_date}
|
{#if collection.start_date && collection.end_date}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue