mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2026-05-18 11:47:04 -04:00
Enhance collection management with modal updates and item handling
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { Collection } from '$lib/types';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import LocationCard from '$lib/components/cards/LocationCard.svelte';
|
||||
import TransportationCard from '$lib/components/cards/TransportationCard.svelte';
|
||||
import LodgingCard from '$lib/components/cards/LodgingCard.svelte';
|
||||
@@ -8,12 +9,13 @@
|
||||
import Magnify from '~icons/mdi/magnify';
|
||||
import ClipboardList from '~icons/mdi/clipboard-list';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let collection: Collection;
|
||||
export let user: any;
|
||||
export let isFolderView: boolean = false;
|
||||
|
||||
// Whether the current user can modify this collection (owner or shared user)
|
||||
export let canModify: boolean = false;
|
||||
|
||||
// Exported so a parent can bind to them if desired
|
||||
export let locationSearch: string = '';
|
||||
@@ -94,217 +96,284 @@
|
||||
c.name.toLowerCase().includes(checklistSearch.toLowerCase())
|
||||
);
|
||||
})();
|
||||
|
||||
// Generic handlers for editing and deleting items in the collection.
|
||||
// `type` should match the collection property name: 'locations', 'transportations', 'lodging', 'notes', 'checklists'
|
||||
function handleItemDelete(type: string, detail: any) {
|
||||
const id = detail?.id ?? detail;
|
||||
if (!id) return;
|
||||
|
||||
const arr = (collection as any)[type];
|
||||
if (!arr || !Array.isArray(arr)) return;
|
||||
|
||||
(collection as any) = {
|
||||
...(collection as any),
|
||||
[type]: arr.filter((item: any) => String(item.id) !== String(id))
|
||||
};
|
||||
}
|
||||
|
||||
function handleItemEdit(type: string, detail: any) {
|
||||
const updated = detail;
|
||||
if (!updated || !updated.id) return;
|
||||
|
||||
const arr = (collection as any)[type];
|
||||
if (!arr || !Array.isArray(arr)) return;
|
||||
|
||||
(collection as any) = {
|
||||
...(collection as any),
|
||||
[type]: arr.map((item: any) => (String(item.id) === String(updated.id) ? updated : item))
|
||||
};
|
||||
|
||||
// Bubble up so parent can open edit modals
|
||||
dispatch('openEdit', { type, item: updated });
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if collection.locations && collection.locations.length > 0}
|
||||
<div class="card bg-base-200 shadow-xl">
|
||||
<div class="card-body">
|
||||
<div class="flex flex-wrap justify-between items-center gap-4 mb-6">
|
||||
<h2 class="card-title text-2xl">
|
||||
📍 Locations ({sortedLocations.length}/{collection.locations.length})
|
||||
</h2>
|
||||
<!-- Show each section as its own card so transportations and others
|
||||
render even when there are no locations. Use vertical spacing
|
||||
between cards via a wrapper. -->
|
||||
<div class="space-y-6">
|
||||
{#if collection.locations && collection.locations.length > 0}
|
||||
<div class="card bg-base-200 shadow-xl">
|
||||
<div class="card-body">
|
||||
<div class="flex flex-wrap justify-between items-center gap-4 mb-6">
|
||||
<h2 class="card-title text-2xl">
|
||||
📍 Locations ({sortedLocations.length}/{collection.locations.length})
|
||||
</h2>
|
||||
|
||||
{#if isFolderView}
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<!-- Search -->
|
||||
<div class="join">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search locations..."
|
||||
class="input input-sm input-bordered join-item w-48"
|
||||
bind:value={locationSearch}
|
||||
/>
|
||||
{#if isFolderView}
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<!-- Search -->
|
||||
<div class="join">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search locations..."
|
||||
class="input input-sm input-bordered join-item w-48"
|
||||
bind:value={locationSearch}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Sort dropdown -->
|
||||
<select class="select select-sm select-bordered" bind:value={locationSort}>
|
||||
<option value="alphabetical-asc">A → Z</option>
|
||||
<option value="alphabetical-desc">Z → A</option>
|
||||
<option value="visited">Visited First</option>
|
||||
<option value="date-asc">Oldest First</option>
|
||||
<option value="date-desc">Newest First</option>
|
||||
</select>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Sort dropdown -->
|
||||
<select class="select select-sm select-bordered" bind:value={locationSort}>
|
||||
<option value="alphabetical-asc">A → Z</option>
|
||||
<option value="alphabetical-desc">Z → A</option>
|
||||
<option value="visited">Visited First</option>
|
||||
<option value="date-asc">Oldest First</option>
|
||||
<option value="date-desc">Newest First</option>
|
||||
</select>
|
||||
<div
|
||||
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 auto-rows-fr items-stretch"
|
||||
>
|
||||
{#each sortedLocations as location}
|
||||
<LocationCard
|
||||
adventure={location}
|
||||
{user}
|
||||
{collection}
|
||||
on:delete={(e) => handleItemDelete('locations', e.detail)}
|
||||
on:edit={(e) => handleItemEdit('locations', e.detail)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if sortedLocations.length === 0}
|
||||
<div class="text-center py-8 opacity-70">
|
||||
<p>No locations match your search</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 auto-rows-fr items-stretch">
|
||||
{#each sortedLocations as location}
|
||||
<LocationCard adventure={location} {user} {collection} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if sortedLocations.length === 0}
|
||||
<div class="text-center py-8 opacity-70">
|
||||
<p>No locations match your search</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Transportations Section -->
|
||||
{#if collection.transportations && collection.transportations.length > 0}
|
||||
<div class="card bg-base-200 shadow-xl mt-6">
|
||||
<div class="card-body">
|
||||
<div class="flex flex-wrap justify-between items-center gap-4 mb-6">
|
||||
<h2 class="card-title text-2xl">
|
||||
✈️ Transportation ({filteredTransportations.length}/{collection.transportations
|
||||
.length})
|
||||
</h2>
|
||||
|
||||
{#if isFolderView}
|
||||
<div class="join">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search transportation..."
|
||||
class="input input-sm input-bordered join-item w-48"
|
||||
bind:value={transportationSearch}
|
||||
/>
|
||||
<button class="btn btn-sm btn-square join-item">
|
||||
<Magnify class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 auto-rows-fr items-stretch"
|
||||
>
|
||||
{#each filteredTransportations as transport}
|
||||
<TransportationCard transportation={transport} {user} {collection} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if filteredTransportations.length === 0}
|
||||
<div class="text-center py-8 opacity-70">
|
||||
<p>No transportation matches your search</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Lodging Section -->
|
||||
{#if collection.lodging && collection.lodging.length > 0}
|
||||
<div class="card bg-base-200 shadow-xl mt-6">
|
||||
<div class="card-body">
|
||||
<div class="flex flex-wrap justify-between items-center gap-4 mb-6">
|
||||
<h2 class="card-title text-2xl">
|
||||
🏨 Lodging ({filteredLodging.length}/{collection.lodging.length})
|
||||
</h2>
|
||||
|
||||
{#if isFolderView}
|
||||
<div class="join">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search lodging..."
|
||||
class="input input-sm input-bordered join-item w-48"
|
||||
bind:value={lodgingSearch}
|
||||
/>
|
||||
<button class="btn btn-sm btn-square join-item">
|
||||
<Magnify class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 auto-rows-fr items-stretch"
|
||||
>
|
||||
{#each filteredLodging as lodging}
|
||||
<LodgingCard {lodging} {user} {collection} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if filteredLodging.length === 0}
|
||||
<div class="text-center py-8 opacity-70">
|
||||
<p>No lodging matches your search</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Notes Section -->
|
||||
{#if collection.notes && collection.notes.length > 0}
|
||||
<div class="card bg-base-200 shadow-xl mt-6">
|
||||
<div class="card-body">
|
||||
<div class="flex flex-wrap justify-between items-center gap-4 mb-6">
|
||||
<h2 class="card-title text-2xl">
|
||||
📝 Notes ({filteredNotes.length}/{collection.notes.length})
|
||||
</h2>
|
||||
|
||||
{#if isFolderView}
|
||||
<div class="join">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search notes..."
|
||||
class="input input-sm input-bordered join-item w-48"
|
||||
bind:value={noteSearch}
|
||||
/>
|
||||
<button class="btn btn-sm btn-square join-item">
|
||||
<Magnify class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 auto-rows-fr items-stretch"
|
||||
>
|
||||
{#each filteredNotes as note}
|
||||
<NoteCard {note} {user} {collection} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if filteredNotes.length === 0}
|
||||
<div class="text-center py-8 opacity-70">
|
||||
<p>No notes match your search</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Checklists Section -->
|
||||
{#if collection.checklists && collection.checklists.length > 0}
|
||||
<div class="card bg-base-200 shadow-xl mt-6">
|
||||
<div class="card-body">
|
||||
<div class="flex flex-wrap justify-between items-center gap-4 mb-6">
|
||||
<h2 class="card-title text-2xl">
|
||||
<ClipboardList class="w-6 h-6" />
|
||||
Checklists ({filteredChecklists.length}/{collection.checklists.length})
|
||||
</h2>
|
||||
|
||||
{#if isFolderView}
|
||||
<div class="join">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search checklists..."
|
||||
class="input input-sm input-bordered join-item w-48"
|
||||
bind:value={checklistSearch}
|
||||
/>
|
||||
<button class="btn btn-sm btn-square join-item">
|
||||
<Magnify class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 auto-rows-fr items-stretch"
|
||||
>
|
||||
{#each filteredChecklists as checklist}
|
||||
<ChecklistCard {checklist} {user} {collection} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if filteredChecklists.length === 0}
|
||||
<div class="text-center py-8 opacity-70">
|
||||
<p>No checklists match your search</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- Transportations Section -->
|
||||
{#if collection.transportations && collection.transportations.length > 0}
|
||||
<div class="card bg-base-200 shadow-xl">
|
||||
<div class="card-body">
|
||||
<div class="flex flex-wrap justify-between items-center gap-4 mb-6">
|
||||
<h2 class="card-title text-2xl">
|
||||
✈️ Transportation ({filteredTransportations.length}/{collection.transportations.length})
|
||||
</h2>
|
||||
|
||||
{#if isFolderView}
|
||||
<div class="join">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search transportation..."
|
||||
class="input input-sm input-bordered join-item w-48"
|
||||
bind:value={transportationSearch}
|
||||
/>
|
||||
<button class="btn btn-sm btn-square join-item">
|
||||
<Magnify class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 auto-rows-fr items-stretch"
|
||||
>
|
||||
{#each filteredTransportations as transport}
|
||||
<TransportationCard
|
||||
transportation={transport}
|
||||
{user}
|
||||
{collection}
|
||||
on:delete={(e) => handleItemDelete('transportations', e.detail)}
|
||||
on:edit={(e) => handleItemEdit('transportations', e.detail)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if filteredTransportations.length === 0}
|
||||
<div class="text-center py-8 opacity-70">
|
||||
<p>No transportation matches your search</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Lodging Section -->
|
||||
{#if collection.lodging && collection.lodging.length > 0}
|
||||
<div class="card bg-base-200 shadow-xl">
|
||||
<div class="card-body">
|
||||
<div class="flex flex-wrap justify-between items-center gap-4 mb-6">
|
||||
<h2 class="card-title text-2xl">
|
||||
🏨 Lodging ({filteredLodging.length}/{collection.lodging.length})
|
||||
</h2>
|
||||
|
||||
{#if isFolderView}
|
||||
<div class="join">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search lodging..."
|
||||
class="input input-sm input-bordered join-item w-48"
|
||||
bind:value={lodgingSearch}
|
||||
/>
|
||||
<button class="btn btn-sm btn-square join-item">
|
||||
<Magnify class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 auto-rows-fr items-stretch"
|
||||
>
|
||||
{#each filteredLodging as lodging}
|
||||
<LodgingCard
|
||||
{lodging}
|
||||
{user}
|
||||
{collection}
|
||||
on:delete={(e) => handleItemDelete('lodging', e.detail)}
|
||||
on:edit={(e) => handleItemEdit('lodging', e.detail)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if filteredLodging.length === 0}
|
||||
<div class="text-center py-8 opacity-70">
|
||||
<p>No lodging matches your search</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Notes Section -->
|
||||
{#if collection.notes && collection.notes.length > 0}
|
||||
<div class="card bg-base-200 shadow-xl">
|
||||
<div class="card-body">
|
||||
<div class="flex flex-wrap justify-between items-center gap-4 mb-6">
|
||||
<h2 class="card-title text-2xl">
|
||||
📝 Notes ({filteredNotes.length}/{collection.notes.length})
|
||||
</h2>
|
||||
|
||||
{#if isFolderView}
|
||||
<div class="join">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search notes..."
|
||||
class="input input-sm input-bordered join-item w-48"
|
||||
bind:value={noteSearch}
|
||||
/>
|
||||
<button class="btn btn-sm btn-square join-item">
|
||||
<Magnify class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 auto-rows-fr items-stretch"
|
||||
>
|
||||
{#each filteredNotes as note}
|
||||
<NoteCard
|
||||
{note}
|
||||
{user}
|
||||
{collection}
|
||||
on:delete={(e) => handleItemDelete('notes', e.detail)}
|
||||
on:edit={(e) => handleItemEdit('notes', e.detail)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if filteredNotes.length === 0}
|
||||
<div class="text-center py-8 opacity-70">
|
||||
<p>No notes match your search</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Checklists Section -->
|
||||
{#if collection.checklists && collection.checklists.length > 0}
|
||||
<div class="card bg-base-200 shadow-xl">
|
||||
<div class="card-body">
|
||||
<div class="flex flex-wrap justify-between items-center gap-4 mb-6">
|
||||
<h2 class="card-title text-2xl">
|
||||
<ClipboardList class="w-6 h-6" />
|
||||
Checklists ({filteredChecklists.length}/{collection.checklists.length})
|
||||
</h2>
|
||||
|
||||
{#if isFolderView}
|
||||
<div class="join">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search checklists..."
|
||||
class="input input-sm input-bordered join-item w-48"
|
||||
bind:value={checklistSearch}
|
||||
/>
|
||||
<button class="btn btn-sm btn-square join-item">
|
||||
<Magnify class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 auto-rows-fr items-stretch"
|
||||
>
|
||||
{#each filteredChecklists as checklist}
|
||||
<ChecklistCard
|
||||
{checklist}
|
||||
{user}
|
||||
{collection}
|
||||
on:delete={(e) => handleItemDelete('checklists', e.detail)}
|
||||
on:edit={(e) => handleItemEdit('checklists', e.detail)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if filteredChecklists.length === 0}
|
||||
<div class="text-center py-8 opacity-70">
|
||||
<p>No checklists match your search</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
|
||||
let modal: HTMLDialogElement;
|
||||
|
||||
// Whether a save/create occurred during this modal session
|
||||
let didSave = false;
|
||||
|
||||
let steps = [
|
||||
{
|
||||
name: $t('adventures.quick_start'),
|
||||
@@ -116,6 +119,15 @@
|
||||
});
|
||||
|
||||
function close() {
|
||||
// If a save occurred, notify the parent with appropriate event
|
||||
if (didSave) {
|
||||
if (locationToEdit) {
|
||||
dispatch('save', location);
|
||||
} else {
|
||||
dispatch('create', location);
|
||||
}
|
||||
}
|
||||
|
||||
dispatch('close');
|
||||
}
|
||||
|
||||
@@ -289,6 +301,9 @@
|
||||
location.user = e.detail.user;
|
||||
location.id = e.detail.id;
|
||||
|
||||
// Mark that a save occurred so close() will notify parent
|
||||
didSave = true;
|
||||
|
||||
steps[1].selected = false;
|
||||
steps[2].selected = true;
|
||||
}}
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
|
||||
let modal: HTMLDialogElement;
|
||||
|
||||
// Whether a save/create occurred during this modal session
|
||||
let didSave = false;
|
||||
|
||||
let steps = [
|
||||
{
|
||||
name: $t('adventures.details'),
|
||||
@@ -116,6 +119,15 @@
|
||||
});
|
||||
|
||||
function close() {
|
||||
// If a save occurred, notify the parent with appropriate event
|
||||
if (didSave) {
|
||||
if (lodgingToEdit) {
|
||||
dispatch('save', lodging);
|
||||
} else {
|
||||
dispatch('create', lodging);
|
||||
}
|
||||
}
|
||||
|
||||
dispatch('close');
|
||||
}
|
||||
|
||||
@@ -235,6 +247,9 @@
|
||||
// Update the entire lodging object with all saved data
|
||||
lodging = { ...lodging, ...e.detail };
|
||||
|
||||
// Mark that a save occurred so close() will notify parent
|
||||
didSave = true;
|
||||
|
||||
// Only allow moving to Media once we have a persisted id.
|
||||
if (!lodging?.id) {
|
||||
addToast('error', $t('adventures.lodging_save_error'));
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
|
||||
let modal: HTMLDialogElement;
|
||||
|
||||
// Whether a save/create occurred during this modal session
|
||||
let didSave = false;
|
||||
|
||||
let steps = [
|
||||
{
|
||||
name: $t('adventures.details'),
|
||||
@@ -128,6 +131,15 @@
|
||||
});
|
||||
|
||||
function close() {
|
||||
// If a save occurred, notify the parent with appropriate event
|
||||
if (didSave) {
|
||||
if (transportationToEdit) {
|
||||
dispatch('save', transportation);
|
||||
} else {
|
||||
dispatch('create', transportation);
|
||||
}
|
||||
}
|
||||
|
||||
dispatch('close');
|
||||
}
|
||||
|
||||
@@ -249,6 +261,9 @@
|
||||
// Update the entire transportation object with all saved data
|
||||
transportation = { ...transportation, ...e.detail };
|
||||
|
||||
// Mark that a save occurred so close() will notify parent
|
||||
didSave = true;
|
||||
|
||||
// Only allow moving to Media once we have a persisted id.
|
||||
if (!transportation?.id) {
|
||||
addToast('error', $t('adventures.lodging_save_error'));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export let appVersion = 'v0.12.0-pre-dev-122825';
|
||||
export let appVersion = 'v0.12.0-pre-dev-123025';
|
||||
export let versionChangelog = 'https://github.com/seanmorley15/AdventureLog/releases/tag/v0.11.0';
|
||||
export let appTitle = 'AdventureLog';
|
||||
export let copyrightYear = '2023-2025';
|
||||
|
||||
@@ -25,6 +25,11 @@
|
||||
import Lightbulb from '~icons/mdi/lightbulb';
|
||||
import Plus from '~icons/mdi/plus';
|
||||
import { addToast } from '$lib/toasts';
|
||||
import NoteModal from '$lib/components/NoteModal.svelte';
|
||||
import ChecklistModal from '$lib/components/ChecklistModal.svelte';
|
||||
import LodgingModal from '$lib/components/lodging/LodgingModal.svelte';
|
||||
import TransportationModal from '$lib/components/transportation/TransportationModal.svelte';
|
||||
import LocationModal from '$lib/components/locations/LocationModal.svelte';
|
||||
|
||||
const renderMarkdown = (markdown: string) => {
|
||||
return marked(markdown) as string;
|
||||
@@ -36,12 +41,42 @@
|
||||
let collection: Collection = (data.props as any).collection || (data.props as any).adventure;
|
||||
let currentSlide = 0;
|
||||
let notFound: boolean = false;
|
||||
let isEditModalOpen: boolean = false;
|
||||
let isLocationModalOpen: boolean = false;
|
||||
let isLodgingModalOpen: boolean = false;
|
||||
let isTransportationModalOpen: boolean = false;
|
||||
let isChecklistModalOpen: boolean = false;
|
||||
let isNoteModalOpen: boolean = false;
|
||||
// Edit placeholders used when creating new items from FAB dropdown
|
||||
let adventureToEdit: any = null;
|
||||
let transportationToEdit: any = null;
|
||||
let noteToEdit: any = null;
|
||||
let checklistToEdit: any = null;
|
||||
let lodgingToEdit: any = null;
|
||||
let heroImages: ContentImage[] = [];
|
||||
let modalInitialIndex: number = 0;
|
||||
let isImageModalOpen: boolean = false;
|
||||
let isLocationLinkModalOpen: boolean = false;
|
||||
|
||||
// Shared helpers for keeping collection sub-items in sync after modal actions
|
||||
type CollectionArrayKey = 'locations' | 'transportations' | 'lodging' | 'notes' | 'checklists';
|
||||
|
||||
function ensureCollectionArray(key: CollectionArrayKey) {
|
||||
if (!collection) return [] as any[];
|
||||
if (!(collection as any)[key]) {
|
||||
(collection as any)[key] = [];
|
||||
}
|
||||
return (collection as any)[key] as any[];
|
||||
}
|
||||
|
||||
function upsertCollectionItem(key: CollectionArrayKey, item: any) {
|
||||
if (!item || item.id === undefined || item.id === null) return;
|
||||
const items = ensureCollectionArray(key);
|
||||
const exists = items.some((entry: any) => String(entry.id) === String(item.id));
|
||||
(collection as any)[key] = exists
|
||||
? items.map((entry: any) => (String(entry.id) === String(item.id) ? item : entry))
|
||||
: [...items, item];
|
||||
}
|
||||
|
||||
// View state from URL params
|
||||
type ViewType = 'all' | 'itinerary' | 'map' | 'recommendations';
|
||||
let currentView: ViewType = 'itinerary';
|
||||
@@ -154,6 +189,35 @@
|
||||
isLocationLinkModalOpen = false;
|
||||
}
|
||||
|
||||
function handleOpenEdit(event: CustomEvent<{ type: CollectionArrayKey; item: any }>) {
|
||||
const { type, item } = event.detail;
|
||||
|
||||
switch (type) {
|
||||
case 'locations':
|
||||
adventureToEdit = item;
|
||||
isLocationModalOpen = true;
|
||||
break;
|
||||
case 'transportations':
|
||||
transportationToEdit = item;
|
||||
isTransportationModalOpen = true;
|
||||
break;
|
||||
case 'lodging':
|
||||
lodgingToEdit = item;
|
||||
isLodgingModalOpen = true;
|
||||
break;
|
||||
case 'notes':
|
||||
noteToEdit = item;
|
||||
isNoteModalOpen = true;
|
||||
break;
|
||||
case 'checklists':
|
||||
checklistToEdit = item;
|
||||
isChecklistModalOpen = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLocationAdded(event: CustomEvent<Location>) {
|
||||
// Link the location to this collection
|
||||
const location = event.detail;
|
||||
@@ -238,6 +302,115 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if isNoteModalOpen}
|
||||
<NoteModal
|
||||
on:close={() => {
|
||||
noteToEdit = null;
|
||||
isNoteModalOpen = false;
|
||||
}}
|
||||
note={noteToEdit}
|
||||
{collection}
|
||||
on:save={(e) => {
|
||||
upsertCollectionItem('notes', e.detail);
|
||||
noteToEdit = null;
|
||||
isNoteModalOpen = false;
|
||||
}}
|
||||
on:create={(e) => {
|
||||
upsertCollectionItem('notes', e.detail);
|
||||
noteToEdit = null;
|
||||
isNoteModalOpen = false;
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if isLocationModalOpen}
|
||||
<LocationModal
|
||||
on:close={() => {
|
||||
adventureToEdit = null;
|
||||
isLocationModalOpen = false;
|
||||
}}
|
||||
user={data.user}
|
||||
{collection}
|
||||
locationToEdit={adventureToEdit}
|
||||
on:save={(e) => {
|
||||
upsertCollectionItem('locations', e.detail);
|
||||
adventureToEdit = null;
|
||||
isLocationModalOpen = false;
|
||||
}}
|
||||
on:create={(e) => {
|
||||
upsertCollectionItem('locations', e.detail);
|
||||
adventureToEdit = null;
|
||||
isLocationModalOpen = false;
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if isTransportationModalOpen}
|
||||
<TransportationModal
|
||||
on:close={() => {
|
||||
transportationToEdit = null;
|
||||
isTransportationModalOpen = false;
|
||||
}}
|
||||
user={data.user}
|
||||
{collection}
|
||||
{transportationToEdit}
|
||||
on:save={(e) => {
|
||||
upsertCollectionItem('transportations', e.detail);
|
||||
transportationToEdit = null;
|
||||
isTransportationModalOpen = false;
|
||||
}}
|
||||
on:create={(e) => {
|
||||
upsertCollectionItem('transportations', e.detail);
|
||||
transportationToEdit = null;
|
||||
isTransportationModalOpen = false;
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if isChecklistModalOpen}
|
||||
<ChecklistModal
|
||||
on:close={() => {
|
||||
checklistToEdit = null;
|
||||
isChecklistModalOpen = false;
|
||||
}}
|
||||
{collection}
|
||||
user={data.user}
|
||||
checklist={checklistToEdit}
|
||||
on:save={(e) => {
|
||||
upsertCollectionItem('checklists', e.detail);
|
||||
checklistToEdit = null;
|
||||
isChecklistModalOpen = false;
|
||||
}}
|
||||
on:create={(e) => {
|
||||
upsertCollectionItem('checklists', e.detail);
|
||||
checklistToEdit = null;
|
||||
isChecklistModalOpen = false;
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if isLodgingModalOpen}
|
||||
<LodgingModal
|
||||
on:close={() => {
|
||||
lodgingToEdit = null;
|
||||
isLodgingModalOpen = false;
|
||||
}}
|
||||
{collection}
|
||||
user={data.user}
|
||||
{lodgingToEdit}
|
||||
on:save={(e) => {
|
||||
upsertCollectionItem('lodging', e.detail);
|
||||
lodgingToEdit = null;
|
||||
isLodgingModalOpen = false;
|
||||
}}
|
||||
on:create={(e) => {
|
||||
upsertCollectionItem('lodging', e.detail);
|
||||
lodgingToEdit = null;
|
||||
isLodgingModalOpen = false;
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if !collection && !notFound}
|
||||
<div class="hero min-h-screen overflow-x-hidden">
|
||||
<div class="hero-content">
|
||||
@@ -434,12 +607,12 @@
|
||||
{/if}
|
||||
|
||||
<!-- All Items View -->
|
||||
{#if currentView === 'all' && collection.locations && collection.locations.length > 0}
|
||||
{#if currentView === 'all'}
|
||||
<CollectionAllItems
|
||||
{collection}
|
||||
bind:collection
|
||||
user={data.user}
|
||||
{isFolderView}
|
||||
canModify={canModifyCollection}
|
||||
on:openEdit={handleOpenEdit}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -678,15 +851,82 @@
|
||||
{/if}
|
||||
|
||||
<!-- Floating Action Button (FAB) - Only shown if user can modify collection -->
|
||||
{#if collection && canModifyCollection}
|
||||
<div class="fixed bottom-6 right-6 z-40">
|
||||
<button
|
||||
class="btn btn-primary btn-circle w-16 h-16 shadow-2xl hover:shadow-primary/25 transition-all duration-200"
|
||||
on:click={openLocationLinkModal}
|
||||
aria-label="Add locations to collection"
|
||||
>
|
||||
<Plus class="w-8 h-8" />
|
||||
</button>
|
||||
{#if collection && canModifyCollection && !collection.is_archived}
|
||||
<div class="fixed bottom-4 right-4 z-[999]">
|
||||
<div class="flex flex-row items-center justify-center gap-4">
|
||||
<div class="dropdown dropdown-top dropdown-end z-[999]">
|
||||
<div tabindex="0" role="button" class="btn m-1 size-16 btn-primary">
|
||||
<Plus class="w-8 h-8" />
|
||||
</div>
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="dropdown-content z-[1] menu p-4 shadow bg-base-300 text-base-content rounded-box w-52 gap-4"
|
||||
>
|
||||
<p class="text-center font-bold text-lg">{$t('adventures.link_new')}</p>
|
||||
<!-- Link existing location to collection -->
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
on:click={() => {
|
||||
isLocationLinkModalOpen = true;
|
||||
}}
|
||||
>
|
||||
{$t('locations.location')}
|
||||
</button>
|
||||
|
||||
<p class="text-center font-bold text-lg">{$t('adventures.add_new')}</p>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
on:click={() => {
|
||||
isLocationModalOpen = true;
|
||||
adventureToEdit = null;
|
||||
}}
|
||||
>
|
||||
{$t('locations.location')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
on:click={() => {
|
||||
transportationToEdit = null;
|
||||
isTransportationModalOpen = true;
|
||||
}}
|
||||
>
|
||||
{$t('adventures.transportation')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
on:click={() => {
|
||||
isNoteModalOpen = true;
|
||||
noteToEdit = null;
|
||||
}}
|
||||
>
|
||||
{$t('adventures.note')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
on:click={() => {
|
||||
checklistToEdit = null;
|
||||
isChecklistModalOpen = true;
|
||||
}}
|
||||
>
|
||||
{$t('adventures.checklist')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
on:click={() => {
|
||||
lodgingToEdit = null;
|
||||
isLodgingModalOpen = true;
|
||||
}}
|
||||
>
|
||||
{$t('adventures.lodging')}
|
||||
</button>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user