fix(frontend): simplify collections view and restore invite access

Unify collections and shared items under a single Collections tab while keeping Archive separate, and fix card layering so menus render correctly. Restore invite discoverability by adding navbar access to /invites and add missing i18n keys to prevent raw key labels in collections/invites UI.
This commit is contained in:
2026-03-08 01:29:52 +00:00
parent f11a5051c6
commit 9eb0325c7a
24 changed files with 116 additions and 266 deletions

View File

@@ -53,21 +53,21 @@
/>
{/if}
<figure>
<figure class="m-0 h-full">
{#if sortedImages && sortedImages.length > 0}
<div class="carousel w-full relative">
<div class="carousel w-full h-full relative">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="carousel-item w-full block">
<div class="carousel-item w-full h-full block">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-missing-attribute -->
<a
on:click|stopPropagation={() => openImageModal(currentSlide)}
class="cursor-pointer relative group"
class="cursor-pointer relative group block h-full w-full"
>
<img
src={sortedImages[currentSlide].image}
class="w-full h-48 object-cover transition-all group-hover:brightness-110"
class="w-full h-full object-cover transition-all group-hover:brightness-110"
alt={name || 'Image'}
/>
@@ -165,7 +165,7 @@
</div>
{:else}
<!-- Fallback with emoji icon as main image -->
<div class="w-full h-48 relative flex items-center justify-center">
<div class="w-full h-full relative flex items-center justify-center">
{#if icon}
<!-- Clean background with emoji as the focal point -->
<div

View File

@@ -113,6 +113,7 @@
const navigationItems = [
{ path: '/locations', icon: MapMarker, label: 'locations.locations' },
{ path: '/collections', icon: FormatListBulletedSquare, label: 'navbar.collections' },
{ path: '/invites', icon: AccountMultiple, label: 'invites.title' },
{ path: '/worldtravel', icon: Earth, label: 'navbar.worldtravel' },
{ path: '/map', icon: MapIcon, label: 'navbar.map' },
{ path: '/calendar', icon: Calendar, label: 'navbar.calendar' },

View File

@@ -192,6 +192,12 @@
}
let isWarningModalOpen: boolean = false;
$: isOwner = !!user && String(user.uuid) === String(collection.user);
$: isSharedMember =
!!user &&
Array.isArray(collection.shared_with) &&
collection.shared_with.some((sharedUserId) => String(sharedUserId) === String(user.uuid));
</script>
{#if isWarningModalOpen}
@@ -210,18 +216,20 @@
{/if}
<div
class="bg-base-100 rounded-2xl shadow hover:shadow-xl transition-all overflow-hidden w-full cursor-pointer group"
class="bg-base-100 rounded-2xl shadow hover:shadow-xl transition-all w-full cursor-pointer group"
role="link"
aria-label={collection.name}
tabindex="0"
on:click={goToCollection}
on:keydown={handleCardKeydown}
>
<div class="relative h-56 overflow-hidden card-carousel-tall">
<div class="relative h-56 card-carousel-tall rounded-t-2xl">
<div class="absolute inset-0 overflow-hidden rounded-t-2xl">
<CardCarousel images={location_images} name={collection.name} icon="📚" />
<div
class="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent pointer-events-none z-10"
></div>
</div>
<div class="absolute top-3 left-3 z-20 flex gap-1.5">
{#if collection.status === 'folder'}
@@ -267,7 +275,7 @@
{/if}
</div>
<div class="absolute top-3 right-3 z-20 flex items-center gap-1.5">
<div class="absolute top-3 right-3 z-30 flex items-center gap-1.5">
<div
class="tooltip tooltip-left"
data-tip={collection.is_public ? $t('adventures.public') : $t('adventures.private')}
@@ -284,8 +292,8 @@
</div>
</div>
{#if user && user.uuid == collection.user && type != 'link' && type != 'viewonly'}
<div class="dropdown dropdown-end">
{#if isOwner && type != 'link' && type != 'viewonly'}
<div class="dropdown dropdown-end z-30">
<button
type="button"
class="btn btn-ghost bg-black/40 backdrop-blur-sm text-white rounded-full w-7 h-7 p-0 min-h-0 border-0 hover:bg-black/55"
@@ -294,7 +302,7 @@
<DotsHorizontal class="w-4 h-4" />
</button>
<ul
class="dropdown-content menu bg-base-100 rounded-box z-[1] w-64 p-2 shadow-xl border border-base-300 mt-1"
class="dropdown-content menu bg-base-100 rounded-box z-[60] w-64 p-2 shadow-xl border border-base-300 mt-1"
>
{#if type != 'viewonly'}
<li>
@@ -380,8 +388,8 @@
{/if}
</ul>
</div>
{:else if user && collection.shared_with && collection.shared_with.includes(user.uuid) && type != 'link'}
<div class="dropdown dropdown-end">
{:else if isSharedMember && type != 'link'}
<div class="dropdown dropdown-end z-30">
<button
type="button"
class="btn btn-ghost bg-black/40 backdrop-blur-sm text-white rounded-full w-7 h-7 p-0 min-h-0 border-0 hover:bg-black/55"
@@ -390,7 +398,7 @@
<DotsHorizontal class="w-4 h-4" />
</button>
<ul
class="dropdown-content menu bg-base-100 rounded-box z-[1] w-64 p-2 shadow-xl border border-base-300 mt-1"
class="dropdown-content menu bg-base-100 rounded-box z-[60] w-64 p-2 shadow-xl border border-base-300 mt-1"
>
<li>
<button

View File

@@ -626,6 +626,7 @@
"invites": {
"accept": "يقبل",
"accept_failed": "فشل في قبول الدعوة",
"fetch_failed": "فشل في جلب الدعوات",
"accepted": "دعوة مقبولة",
"by": "بواسطة",
"decline": "انخفاض",
@@ -1081,7 +1082,8 @@
},
"common": {
"show_less": "إخفاء التفاصيل",
"show_more": "عرض المزيد"
"show_more": "عرض المزيد",
"refresh": "تحديث"
},
"currencies": {
"AED": "درهم اماراتي",

View File

@@ -1038,6 +1038,7 @@
"invites": {
"accept": "Akzeptieren",
"accept_failed": "Fehler beim Akzeptieren der Einladung",
"fetch_failed": "Einladungen konnten nicht geladen werden",
"accepted": "Einladung akzeptiert",
"by": "von",
"decline": "Ablehnen",
@@ -1081,7 +1082,8 @@
},
"common": {
"show_less": "Details ausblenden",
"show_more": "Mehr anzeigen"
"show_more": "Mehr anzeigen",
"refresh": "Aktualisieren"
},
"currencies": {
"AED": "VAE-Dirham",

View File

@@ -1040,6 +1040,7 @@
"invites": {
"accepted": "Invite accepted",
"accept_failed": "Failed to accept invite",
"fetch_failed": "Failed to fetch invites",
"declined": "Invite declined",
"decline_failed": "Failed to decline invite",
"title": "Invites",
@@ -1103,7 +1104,8 @@
},
"common": {
"show_less": "Hide details",
"show_more": "Show more"
"show_more": "Show more",
"refresh": "Refresh"
},
"collections": {
"not_found": "Collection Not Found",

View File

@@ -1038,6 +1038,7 @@
"invites": {
"accept": "Aceptar",
"accept_failed": "No se pudo aceptar la invitación",
"fetch_failed": "No se pudieron cargar las invitaciones",
"accepted": "Invite aceptado",
"by": "por",
"decline": "Rechazar",
@@ -1081,7 +1082,8 @@
},
"common": {
"show_less": "Ocultar detalles",
"show_more": "Mostrar más"
"show_more": "Mostrar más",
"refresh": "Actualizar"
},
"currencies": {
"AED": "Dírham de los Emiratos Árabes Unidos",

View File

@@ -1037,6 +1037,7 @@
"settings_download_backup": "Télécharger la sauvegarde",
"invites": {
"accept_failed": "Échec de l'acceptation de l'invitation",
"fetch_failed": "Impossible de récupérer les invitations",
"accepted": "Inviter accepté",
"by": "par",
"decline": "Déclin",
@@ -1081,7 +1082,8 @@
},
"common": {
"show_less": "Masquer les détails",
"show_more": "Afficher plus"
"show_more": "Afficher plus",
"refresh": "Actualiser"
},
"currencies": {
"AED": "Dirham des Émirats Arabes Unis",

View File

@@ -1038,6 +1038,7 @@
"invites": {
"accepted": "Meghívó elfogadva",
"accept_failed": "Nem sikerült elfogadni a meghívót",
"fetch_failed": "Nem sikerült lekérni a meghívókat",
"declined": "Meghívó elutasítva",
"decline_failed": "Nem sikerült elutasítani a meghívót",
"title": "Meghívók",
@@ -1081,7 +1082,8 @@
},
"common": {
"show_less": "Részletek elrejtése",
"show_more": "Mutasson többet"
"show_more": "Mutasson többet",
"refresh": "Frissítés"
},
"currencies": {
"AED": "Egyesült Arab Emírségek dirham",

View File

@@ -1038,6 +1038,7 @@
"invites": {
"accept": "Accettare",
"accept_failed": "Impossibile accettare l'invito",
"fetch_failed": "Impossibile recuperare gli inviti",
"accepted": "Invito accettato",
"by": "di",
"decline": "Declino",
@@ -1081,7 +1082,8 @@
},
"common": {
"show_less": "Nascondi dettagli",
"show_more": "Mostra di più"
"show_more": "Mostra di più",
"refresh": "Aggiorna"
},
"currencies": {
"AED": "Dirham degli Emirati Arabi Uniti",

View File

@@ -626,6 +626,7 @@
"invites": {
"accept": "受け入れる",
"accept_failed": "招待を受け入れなかった",
"fetch_failed": "招待を取得できませんでした",
"accepted": "招待された招待",
"by": "による",
"decline": "衰退",
@@ -1081,7 +1082,8 @@
},
"common": {
"show_less": "詳細を隠す",
"show_more": "もっと見る"
"show_more": "もっと見る",
"refresh": "更新"
},
"currencies": {
"AED": "アラブ首長国連邦ディルハム",

View File

@@ -1038,6 +1038,7 @@
"invites": {
"accept": "수용하다",
"accept_failed": "초대를 수락하지 못했습니다",
"fetch_failed": "초대를 불러오지 못했습니다",
"accepted": "허가를 초대하십시오",
"by": "~에 의해",
"decline": "감소",
@@ -1081,7 +1082,8 @@
},
"common": {
"show_less": "세부정보 숨기기",
"show_more": "더 보기"
"show_more": "더 보기",
"refresh": "새로고침"
},
"currencies": {
"AED": "UAE 디르함",

View File

@@ -1038,6 +1038,7 @@
"invites": {
"accept": "Accepteren",
"accept_failed": "Kan uitnodigen niet accepteren",
"fetch_failed": "Uitnodigingen konden niet worden opgehaald",
"accepted": "Nodig geaccepteerd uit",
"by": "door",
"decline": "Afwijzen",
@@ -1081,7 +1082,8 @@
},
"common": {
"show_less": "Details verbergen",
"show_more": "Laat meer zien"
"show_more": "Laat meer zien",
"refresh": "Vernieuwen"
},
"currencies": {
"AED": "VAE Dirham",

View File

@@ -1038,6 +1038,7 @@
"invites": {
"accept": "Akseptere",
"accept_failed": "Kunne ikke godta invitasjon",
"fetch_failed": "Kunne ikke hente invitasjoner",
"accepted": "Inviter akseptert",
"by": "ved",
"decline": "Avslå",
@@ -1081,7 +1082,8 @@
},
"common": {
"show_less": "Skjul detaljer",
"show_more": "Vis mer"
"show_more": "Vis mer",
"refresh": "Oppdater"
},
"currencies": {
"AED": "UAE Dirham",

View File

@@ -1038,6 +1038,7 @@
"invites": {
"accept": "Przyjąć",
"accept_failed": "Nie udało się zaakceptować zaproszenia",
"fetch_failed": "Nie udało się pobrać zaproszeń",
"accepted": "Zaproś zaakceptowane",
"by": "przez",
"decline": "Spadek",
@@ -1081,7 +1082,8 @@
},
"common": {
"show_less": "Ukryj szczegóły",
"show_more": "Pokaż więcej"
"show_more": "Pokaż więcej",
"refresh": "Odśwież"
},
"currencies": {
"AED": "Dirham Zjednoczonych Emiratów Arabskich",

View File

@@ -626,6 +626,7 @@
"invites": {
"accept": "Aceitar",
"accept_failed": "Falha ao aceitar convite",
"fetch_failed": "Falha ao buscar convites",
"accepted": "Convite Aceito",
"by": "por",
"decline": "Recusar",
@@ -1081,7 +1082,8 @@
},
"common": {
"show_less": "Ocultar detalhes",
"show_more": "Mostrar mais"
"show_more": "Mostrar mais",
"refresh": "Atualizar"
},
"currencies": {
"AED": "Dirham dos Emirados Árabes Unidos",

View File

@@ -618,7 +618,8 @@
},
"common": {
"show_less": "Ascunde detaliile",
"show_more": "Arată mai multe"
"show_more": "Arată mai multe",
"refresh": "Reîmprospătează"
},
"currencies": {
"AED": "Dirhamul Emiratelor Arabe Unite",
@@ -702,6 +703,7 @@
"invites": {
"accept": "Accepta",
"accept_failed": "Nu s-a acceptat invitația",
"fetch_failed": "Nu s-au putut încărca invitațiile",
"accepted": "Invitația a fost acceptată",
"by": "de",
"decline": "Declin",

View File

@@ -1038,6 +1038,7 @@
"invites": {
"accept": "Принять",
"accept_failed": "Не удалось принять приглашение",
"fetch_failed": "Не удалось загрузить приглашения",
"accepted": "Приглашение принято",
"by": "к",
"decline": "Отклонить",
@@ -1081,7 +1082,8 @@
},
"common": {
"show_less": "Скрыть детали",
"show_more": "Показать больше"
"show_more": "Показать больше",
"refresh": "Обновить"
},
"currencies": {
"AED": "Дирхам ОАЭ",

View File

@@ -1038,6 +1038,7 @@
"invites": {
"accepted": "Pozvánka prijatá",
"accept_failed": "Nepodarilo sa prijať pozvánku",
"fetch_failed": "Nepodarilo sa načítať pozvánky",
"declined": "Pozvánka zamietnutá",
"decline_failed": "Nepodarilo sa zamietnuť pozvánku",
"title": "Pozvánky",
@@ -1081,7 +1082,8 @@
},
"common": {
"show_less": "Skryť podrobnosti",
"show_more": "Ukáž viac"
"show_more": "Ukáž viac",
"refresh": "Obnoviť"
},
"currencies": {
"AED": "Dirham SAE",

View File

@@ -1038,6 +1038,7 @@
"invites": {
"accept": "Acceptera",
"accept_failed": "Det gick inte att acceptera inbjudan",
"fetch_failed": "Det gick inte att hämta inbjudningar",
"accepted": "Bjuda in accepterad",
"by": "av",
"decline": "Nedgång",
@@ -1081,7 +1082,8 @@
},
"common": {
"show_less": "Dölj detaljer",
"show_more": "Visa mer"
"show_more": "Visa mer",
"refresh": "Uppdatera"
},
"currencies": {
"AED": "UAE Dirham",

View File

@@ -1038,6 +1038,7 @@
"invites": {
"accepted": "Davet kabul edildi",
"accept_failed": "Davet kabul edilemedi",
"fetch_failed": "Davetler alınamadı",
"declined": "Davet reddedildi",
"decline_failed": "Davet reddedilemedi",
"title": "Davetler",
@@ -1081,7 +1082,8 @@
},
"common": {
"show_less": "Ayrıntıları gizle",
"show_more": "Daha fazlasını göster"
"show_more": "Daha fazlasını göster",
"refresh": "Yenile"
},
"currencies": {
"AED": "BAE Dirhemi",

View File

@@ -626,6 +626,7 @@
"invites": {
"accept": "прийняти",
"accept_failed": "Не вдалося прийняти запрошення",
"fetch_failed": "Не вдалося завантажити запрошення",
"accepted": "Запрошення прийнято",
"by": "за",
"decline": "відхилити",
@@ -1081,7 +1082,8 @@
},
"common": {
"show_less": "Приховати деталі",
"show_more": "Показати більше"
"show_more": "Показати більше",
"refresh": "Оновити"
},
"currencies": {
"AED": "дирхам ОАЕ",

View File

@@ -1038,6 +1038,7 @@
"invites": {
"accept": "接受",
"accept_failed": "未能接受邀请",
"fetch_failed": "无法获取邀请",
"accepted": "邀请接受",
"by": "经过",
"decline": "衰退",
@@ -1066,7 +1067,8 @@
},
"common": {
"show_less": "隐藏详细信息",
"show_more": "显示更多"
"show_more": "显示更多",
"refresh": "刷新"
},
"itinerary": {
"item_remove_error": "从行程中删除项目时出错",

View File

@@ -4,18 +4,14 @@
import { page } from '$app/stores';
import CollectionCard from '$lib/components/cards/CollectionCard.svelte';
import CollectionModal from '$lib/components/CollectionModal.svelte';
import type { Collection, CollectionInvite, SlimCollection } from '$lib/types';
import type { Collection, SlimCollection } from '$lib/types';
import { t } from 'svelte-i18n';
import Plus from '~icons/mdi/plus';
import Filter from '~icons/mdi/filter-variant';
import Sort from '~icons/mdi/sort';
import Archive from '~icons/mdi/archive';
import Share from '~icons/mdi/share-variant';
import CollectionIcon from '~icons/mdi/folder-multiple';
import MailIcon from '~icons/mdi/email';
import CheckIcon from '~icons/mdi/check';
import CloseIcon from '~icons/mdi/close';
import { addToast } from '$lib/toasts';
import DeleteWarning from '$lib/components/DeleteWarning.svelte';
@@ -28,7 +24,7 @@
let newType: string = '';
let resultsPerPage: number = 25;
let isShowingCollectionModal: boolean = false;
let activeView: 'owned' | 'shared' | 'archived' | 'invites' = 'owned';
let activeView: 'collections' | 'archived' = 'collections';
let next: string | null = data.props.next || null;
let previous: string | null = data.props.previous || null;
@@ -39,35 +35,19 @@
let orderDirection = data.props.order_direction || 'asc';
let statusFilter = data.props.status || '';
let invites: CollectionInvite[] = data.props.invites || [];
let sidebarOpen = false;
let collectionToEdit: Collection | null = null;
$: currentCollections =
activeView === 'owned'
? collections
: activeView === 'shared'
? sharedCollections
: activeView === 'archived'
? archivedCollections
: [];
$: mergedCollections = [...collections, ...sharedCollections].filter(
(collection, index, list) => index === list.findIndex((entry) => entry.id === collection.id)
);
$: currentCount =
activeView === 'owned'
? collections.length
: activeView === 'shared'
? sharedCollections.length
: activeView === 'archived'
? archivedCollections.length
: activeView === 'invites'
? invites.length
: 0;
$: currentCollections = activeView === 'collections' ? mergedCollections : archivedCollections;
// Optionally, keep count in sync with collections only for owned view
// Keep count in sync with collections view
$: {
if (activeView === 'owned' && count !== collections.length) {
count = collections.length;
if (activeView === 'collections' && count !== mergedCollections.length) {
count = mergedCollections.length;
}
}
@@ -200,7 +180,7 @@
(collection) => collection.id !== duplicatedCollection.id
);
activeView = 'owned';
activeView = 'collections';
}
async function editCollection(event: CustomEvent<SlimCollection>) {
@@ -252,86 +232,9 @@
sidebarOpen = !sidebarOpen;
}
function switchView(view: 'owned' | 'shared' | 'archived' | 'invites') {
function switchView(view: 'collections' | 'archived') {
activeView = view;
}
// Invite functions
async function acceptInvite(invite: CollectionInvite) {
try {
const res = await fetch(`/api/collections/${invite.collection}/accept-invite/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (res.ok) {
// Try to parse returned collection data
let data: any = null;
try {
data = await res.json();
} catch (e) {
data = null;
}
// Remove invite from list
invites = invites.filter((i) => i.id !== invite.id);
addToast('success', `${$t('invites.accepted')} "${invite.name}"`);
// If API returned the accepted collection, add it to sharedCollections immediately
if (data && (data.collection || data.result || data.id)) {
// Normalize expected shapes: {collection: {...}} or collection object directly
const newCollection = data.collection ? data.collection : data;
// Prepend so it's visible at top
sharedCollections = [newCollection as SlimCollection, ...sharedCollections];
} else {
// Fallback: refresh shared collections from API
try {
const sharedRes = await fetch(`/api/collections/shared/?nested=true`);
if (sharedRes.ok) {
const sharedData = await sharedRes.json();
// Prefer results if paginated
sharedCollections = sharedData.results ? sharedData.results : sharedData;
}
} catch (e) {
// ignore fallback errors; user already got success toast
}
}
} else {
const error = await res.json();
addToast('error', error.error || $t('invites.accept_failed'));
}
} catch (error) {
addToast('error', $t('invites.accept_failed'));
}
}
async function declineInvite(invite: CollectionInvite) {
try {
const res = await fetch(`/api/collections/${invite.collection}/decline-invite/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (res.ok) {
// Remove invite from list
invites = invites.filter((i) => i.id !== invite.id);
addToast('success', `${$t('invites.declined')} "${invite.name}"`);
} else {
const error = await res.json();
addToast('error', error.error || $t('invites.decline_failed'));
}
} catch (error) {
addToast('error', $t('invites.decline_failed'));
}
}
function formatDate(dateString: string): string {
return new Date(dateString).toLocaleDateString('en-GB');
}
</script>
<svelte:head>
@@ -406,25 +309,14 @@
<div class="flex border-b border-base-200 mt-4 overflow-x-auto">
<button
class="tab px-4 py-2 text-sm font-medium border-b-2 transition-colors whitespace-nowrap {activeView ===
'owned'
'collections'
? 'border-primary text-primary'
: 'border-transparent text-base-content/60 hover:text-base-content'}"
on:click={() => switchView('owned')}
on:click={() => switchView('collections')}
>
<CollectionIcon class="w-4 h-4" />
<span class="hidden sm:inline">{$t('adventures.my_collections')}</span>
<div class="badge badge-sm badge-ghost">{collections.length}</div>
</button>
<button
class="tab px-4 py-2 text-sm font-medium border-b-2 transition-colors whitespace-nowrap {activeView ===
'shared'
? 'border-primary text-primary'
: 'border-transparent text-base-content/60 hover:text-base-content'}"
on:click={() => switchView('shared')}
>
<Share class="w-4 h-4" />
<span class="hidden sm:inline">{$t('share.shared')}</span>
<div class="badge badge-sm badge-ghost">{sharedCollections.length}</div>
<span class="hidden sm:inline">{$t('navbar.collections')}</span>
<div class="badge badge-sm badge-ghost">{mergedCollections.length}</div>
</button>
<button
class="tab px-4 py-2 text-sm font-medium border-b-2 transition-colors whitespace-nowrap {activeView ===
@@ -437,121 +329,33 @@
<span class="hidden sm:inline">{$t('adventures.archived')}</span>
<div class="badge badge-sm badge-ghost">{archivedCollections.length}</div>
</button>
<button
class="tab px-4 py-2 text-sm font-medium border-b-2 transition-colors whitespace-nowrap {activeView ===
'invites'
? 'border-primary text-primary'
: 'border-transparent text-base-content/60 hover:text-base-content'}"
on:click={() => switchView('invites')}
>
<div class="indicator">
<MailIcon class="w-4 h-4" />
{#if invites.length > 0}
<span class="indicator-item badge badge-xs badge-error"></span>
{/if}
</div>
<span class="hidden sm:inline">{$t('invites.title')}</span>
<div class="badge badge-sm {invites.length > 0 ? 'badge-error' : 'badge-ghost'}">
{invites.length}
</div>
</button>
</div>
</div>
</div>
<!-- Main Content -->
<div class="container mx-auto px-6 py-8">
{#if activeView === 'invites'}
<!-- Invites Content -->
{#if invites.length === 0}
<div class="flex flex-col items-center justify-center py-16">
<div class="p-6 bg-base-200/50 rounded-2xl mb-6">
<MailIcon class="w-16 h-16 text-base-content/30" />
</div>
<h3 class="text-xl font-semibold text-base-content/70 mb-2">
{$t('invites.no_invites')}
</h3>
<p class="text-base-content/50 text-center max-w-md">
{$t('invites.no_invites_desc')}
</p>
</div>
{:else}
<div class="space-y-4">
{#each invites as invite}
<div
class="card bg-base-100 shadow-lg border border-base-300 hover:shadow-xl transition-shadow"
>
<div class="card-body p-6">
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="flex items-center gap-3 mb-2">
<div class="p-2 bg-primary/10 rounded-lg">
<CollectionIcon class="w-5 h-5 text-primary" />
</div>
<div>
<h3 class="font-semibold text-lg">
{invite.name}
</h3>
<p class="text-xs text-base-content/50">
{$t('invites.invited_on')}
{formatDate(invite.created_at)}
{$t('invites.by')}
{invite.collection_owner_username || ''}
({invite.collection_user_first_name || ''}
{invite.collection_user_last_name || ''})
</p>
</div>
</div>
</div>
<div class="flex gap-2 ml-4">
<button
class="btn btn-success btn-sm gap-2"
on:click={() => acceptInvite(invite)}
>
<CheckIcon class="w-4 h-4" />
{$t('invites.accept')}
</button>
<button
class="btn btn-error btn-sm btn-outline gap-2"
on:click={() => declineInvite(invite)}
>
<CloseIcon class="w-4 h-4" />
{$t('invites.decline')}
</button>
</div>
</div>
</div>
</div>
{/each}
</div>
{/if}
{:else if currentCollections.length === 0}
{#if currentCollections.length === 0}
<!-- Empty State for Collections -->
<div class="flex flex-col items-center justify-center py-16">
<div class="p-6 bg-base-200/50 rounded-2xl mb-6">
{#if activeView === 'owned'}
{#if activeView === 'collections'}
<CollectionIcon class="w-16 h-16 text-base-content/30" />
{:else if activeView === 'shared'}
<Share class="w-16 h-16 text-base-content/30" />
{:else}
<Archive class="w-16 h-16 text-base-content/30" />
{/if}
</div>
<h3 class="text-xl font-semibold text-base-content/70 mb-2">
{activeView === 'owned'
{activeView === 'collections'
? $t('collection.no_collections_yet')
: activeView === 'shared'
? $t('collection.no_shared_collections')
: $t('collection.no_archived_collections')}
</h3>
<p class="text-base-content/50 text-center max-w-md">
{activeView === 'owned'
{activeView === 'collections'
? $t('collection.create_first')
: activeView === 'shared'
? $t('collection.make_sure_public')
: $t('collection.archived_appear_here')}
</p>
{#if activeView === 'owned'}
{#if activeView === 'collections'}
<button
class="btn btn-primary btn-wide mt-6 gap-2"
on:click={() => {
@@ -587,7 +391,7 @@
</div>
<!-- Pagination -->
{#if activeView === 'owned' && (next || previous)}
{#if activeView === 'collections' && (next || previous)}
<div class="flex justify-center mt-12">
<div class="join bg-base-100 shadow-lg rounded-2xl p-2">
{#each Array.from({ length: totalPages }, (_, i) => i + 1) as page}
@@ -620,8 +424,6 @@
<h2 class="text-xl font-bold">{$t('adventures.filters_and_sort')}</h2>
</div>
<!-- Only show sort options for collection views, not invites -->
{#if activeView !== 'invites'}
<!-- Status Filter -->
<div class="card bg-base-200/50 p-4 mb-4">
<h3 class="font-semibold text-lg mb-4 flex items-center gap-2">
@@ -756,14 +558,13 @@
</div>
</div>
</div>
{/if}
</div>
</div>
</div>
</div>
<!-- Floating Action Button -->
{#if activeView === 'owned'}
{#if activeView === 'collections'}
<div class="fixed bottom-6 right-6 z-50">
<div class="dropdown dropdown-top dropdown-end">
<div