1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-07-18 20:39:36 +02:00

feat: Enhance Adventure and Collection Management

- Added support for multiple collections in AdventureSerializer, allowing adventures to be linked to multiple collections.
- Implemented validation to ensure collections belong to the current user during adventure creation and updates.
- Introduced a signal to update adventure publicity based on the public status of linked collections.
- Updated file permission checks to consider multiple collections when determining access rights.
- Modified AdventureImageViewSet and AttachmentViewSet to check access against collections instead of a single collection.
- Enhanced AdventureViewSet to support filtering and sorting adventures based on collections.
- Updated frontend components to manage collections more effectively, including linking and unlinking adventures from collections.
- Adjusted API endpoints and data structures to accommodate the new collections feature.
- Improved user experience with appropriate notifications for collection actions.
This commit is contained in:
Sean Morley 2025-06-12 15:54:01 -04:00
parent d9070e68bb
commit 3f9a6767bd
22 changed files with 686 additions and 289 deletions

View file

@ -88,38 +88,61 @@
}
}
async function removeFromCollection() {
async function linkCollection(event: CustomEvent<string>) {
let collectionId = event.detail;
// Create a copy to avoid modifying the original directly
const updatedCollections = adventure.collections ? [...adventure.collections] : [];
// Add the new collection if not already present
if (!updatedCollections.some((c) => String(c) === String(collectionId))) {
updatedCollections.push(collectionId);
}
let res = await fetch(`/api/adventures/${adventure.id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ collection: null })
body: JSON.stringify({ collections: updatedCollections })
});
if (res.ok) {
addToast('info', `${$t('adventures.collection_remove_success')}`);
dispatch('delete', adventure.id);
// Only update the adventure.collections after server confirms success
adventure.collections = updatedCollections;
addToast('info', `${$t('adventures.collection_link_success')}`);
} else {
addToast('error', `${$t('adventures.collection_remove_error')}`);
addToast('error', `${$t('adventures.collection_link_error')}`);
}
}
async function linkCollection(event: CustomEvent<number>) {
async function removeFromCollection(event: CustomEvent<string>) {
let collectionId = event.detail;
let res = await fetch(`/api/adventures/${adventure.id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ collection: collectionId })
});
if (res.ok) {
console.log('Adventure linked to collection');
addToast('info', `${$t('adventures.collection_link_success')}`);
isCollectionModalOpen = false;
dispatch('delete', adventure.id);
} else {
addToast('error', `${$t('adventures.collection_link_error')}`);
if (!collectionId) {
addToast('error', `${$t('adventures.collection_remove_error')}`);
return;
}
// Create a copy to avoid modifying the original directly
if (adventure.collections) {
const updatedCollections = adventure.collections.filter(
(c) => String(c) !== String(collectionId)
);
let res = await fetch(`/api/adventures/${adventure.id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ collections: updatedCollections })
});
if (res.ok) {
// Only update adventure.collections after server confirms success
adventure.collections = updatedCollections;
addToast('info', `${$t('adventures.collection_remove_success')}`);
} else {
addToast('error', `${$t('adventures.collection_remove_error')}`);
}
}
}
@ -133,7 +156,12 @@
</script>
{#if isCollectionModalOpen}
<CollectionLink on:link={linkCollection} on:close={() => (isCollectionModalOpen = false)} />
<CollectionLink
on:link={(e) => linkCollection(e)}
on:unlink={(e) => removeFromCollection(e)}
on:close={() => (isCollectionModalOpen = false)}
linkedCollectionList={adventure.collections}
/>
{/if}
{#if isWarningModalOpen}
@ -269,23 +297,14 @@
</button>
</li>
{#if adventure.collection && user?.uuid == adventure.user_id}
<li>
<button on:click={removeFromCollection} class="flex items-center gap-2">
<LinkVariantRemove class="w-4 h-4" />
{$t('adventures.remove_from_collection')}
</button>
</li>
{/if}
{#if !adventure.collection}
{#if user?.uuid == adventure.user_id}
<li>
<button
on:click={() => (isCollectionModalOpen = true)}
class="flex items-center gap-2"
>
<Plus class="w-4 h-4" />
{$t('adventures.add_to_collection')}
{$t('collection.manage_collections')}
</button>
</li>
{/if}

View file

@ -12,21 +12,31 @@
let isLoading: boolean = true;
export let user: User | null;
export let collectionId: string;
onMount(async () => {
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
if (modal) {
modal.showModal();
}
let res = await fetch(`/api/adventures/all/?include_collections=false`, {
let res = await fetch(`/api/adventures/all/?include_collections=true`, {
method: 'GET'
});
const newAdventures = await res.json();
if (res.ok && adventures) {
// Filter out adventures that are already linked to the collections
// basically for each adventure, check if collections array contains the id of the current collection
if (collectionId) {
adventures = newAdventures.filter((adventure: Adventure) => {
// adventure.collections is an array of ids, collectionId is a single id
return !(adventure.collections ?? []).includes(collectionId);
});
} else {
adventures = newAdventures;
}
// No need to reassign adventures to newAdventures here, keep the filtered result
isLoading = false;
});

View file

@ -14,6 +14,7 @@
import { t } from 'svelte-i18n';
import Plus from '~icons/mdi/plus';
import Minus from '~icons/mdi/minus';
import DotsHorizontal from '~icons/mdi/dots-horizontal';
import TrashCan from '~icons/mdi/trashcan';
import DeleteWarning from './DeleteWarning.svelte';
@ -23,6 +24,7 @@
const dispatch = createEventDispatcher();
export let type: String | undefined | null;
export let linkedCollectionList: string[] | null = null;
let isShareModalOpen: boolean = false;
function editAdventure() {
@ -138,10 +140,25 @@
<!-- Actions -->
<div class="pt-4 border-t border-base-300">
{#if type == 'link'}
<button class="btn btn-primary btn-block" on:click={() => dispatch('link', collection.id)}>
<Plus class="w-4 h-4" />
{$t('adventures.add_to_collection')}
</button>
{#if linkedCollectionList && linkedCollectionList
.map(String)
.includes(String(collection.id))}
<button
class="btn btn-error btn-block"
on:click={() => dispatch('unlink', collection.id)}
>
<Minus class="w-4 h-4" />
{$t('adventures.remove_from_collection')}
</button>
{:else}
<button
class="btn btn-primary btn-block"
on:click={() => dispatch('link', collection.id)}
>
<Plus class="w-4 h-4" />
{$t('adventures.add_to_collection')}
</button>
{/if}
{:else}
<div class="flex justify-between items-center">
<button

View file

@ -9,6 +9,8 @@
let collections: Collection[] = [];
export let linkedCollectionList: string[] | null = null;
onMount(async () => {
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
if (modal) {
@ -30,10 +32,14 @@
dispatch('close');
}
function link(event: CustomEvent<number>) {
function link(event: CustomEvent<string>) {
dispatch('link', event.detail);
}
function unlink(event: CustomEvent<string>) {
dispatch('unlink', event.detail);
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
dispatch('close');
@ -46,9 +52,15 @@
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div class="modal-box w-11/12 max-w-5xl" role="dialog" on:keydown={handleKeydown} tabindex="0">
<h1 class="text-center font-bold text-4xl mb-6">{$t('adventures.my_collections')}</h1>
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center mb-4">
{#each collections as collection}
<CollectionCard {collection} type="link" on:link={link} />
<CollectionCard
{collection}
type="link"
on:link={link}
bind:linkedCollectionList
on:unlink={unlink}
/>
{/each}
{#if collections.length === 0}
<p class="text-center text-lg">{$t('adventures.no_collections_found')}</p>

View file

@ -36,7 +36,7 @@ export type Adventure = {
timezone: string | null;
notes: string;
}[];
collection?: string | null;
collections?: string[] | null;
latitude: number | null;
longitude: number | null;
is_public: boolean;

View file

@ -523,7 +523,8 @@
"collection_edit_success": "Collection edited successfully!",
"error_editing_collection": "Error editing collection",
"edit_collection": "Edit Collection",
"public_collection": "Public Collection"
"public_collection": "Public Collection",
"manage_collections": "Manage Collections"
},
"notes": {
"note_deleted": "Note deleted successfully!",

View file

@ -20,22 +20,10 @@ export const load = (async (event) => {
};
} else {
let adventure = (await request.json()) as AdditionalAdventure;
let collection: Collection | null = null;
if (adventure.collection) {
let res2 = await fetch(`${endpoint}/api/collections/${adventure.collection}/`, {
headers: {
Cookie: `sessionid=${event.cookies.get('sessionid')}`
},
credentials: 'include'
});
collection = await res2.json();
}
return {
props: {
adventure,
collection
adventure
}
};
}

View file

@ -360,12 +360,18 @@
? `🌍 ${$t('adventures.public')}`
: `🔒 ${$t('adventures.private')}`}
</div>
{#if data.props.collection}
<!-- {#if data.props.collection}
<div class="badge badge-sm badge-outline">
📚 <a href="/collections/{data.props.collection.id}" class="link"
>{data.props.collection.name}</a
>
</div>
{/if} -->
{#if adventure.collections && adventure.collections.length > 0}
<div class="badge badge-sm badge-outline">
📚
<p>{adventure.collections.length} {$t('navbar.collections')}</p>
</div>
{/if}
</div>
</div>

View file

@ -362,12 +362,21 @@
} else {
let adventure = event.detail;
// add the collection id to the adventure collections array
if (!adventure.collections) {
adventure.collections = [collection.id];
} else {
if (!adventure.collections.includes(collection.id)) {
adventure.collections.push(collection.id);
}
}
let res = await fetch(`/api/adventures/${adventure.id}/`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ collection: collection.id.toString() })
body: JSON.stringify({ collections: adventure.collections })
});
if (res.ok) {
@ -550,6 +559,7 @@
on:close={() => {
isShowingLinkModal = false;
}}
collectionId={collection.id}
on:add={addAdventure}
/>
{/if}