1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-07-22 22: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>