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

View File

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

View File

@@ -192,6 +192,12 @@
} }
let isWarningModalOpen: boolean = false; 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> </script>
{#if isWarningModalOpen} {#if isWarningModalOpen}
@@ -210,18 +216,20 @@
{/if} {/if}
<div <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" role="link"
aria-label={collection.name} aria-label={collection.name}
tabindex="0" tabindex="0"
on:click={goToCollection} on:click={goToCollection}
on:keydown={handleCardKeydown} on:keydown={handleCardKeydown}
> >
<div class="relative h-56 overflow-hidden card-carousel-tall"> <div class="relative h-56 card-carousel-tall rounded-t-2xl">
<CardCarousel images={location_images} name={collection.name} icon="📚" /> <div class="absolute inset-0 overflow-hidden rounded-t-2xl">
<div <CardCarousel images={location_images} name={collection.name} icon="📚" />
class="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent pointer-events-none z-10" <div
></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"> <div class="absolute top-3 left-3 z-20 flex gap-1.5">
{#if collection.status === 'folder'} {#if collection.status === 'folder'}
@@ -267,7 +275,7 @@
{/if} {/if}
</div> </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 <div
class="tooltip tooltip-left" class="tooltip tooltip-left"
data-tip={collection.is_public ? $t('adventures.public') : $t('adventures.private')} data-tip={collection.is_public ? $t('adventures.public') : $t('adventures.private')}
@@ -284,8 +292,8 @@
</div> </div>
</div> </div>
{#if user && user.uuid == collection.user && type != 'link' && type != 'viewonly'} {#if isOwner && type != 'link' && type != 'viewonly'}
<div class="dropdown dropdown-end"> <div class="dropdown dropdown-end z-30">
<button <button
type="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" 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" /> <DotsHorizontal class="w-4 h-4" />
</button> </button>
<ul <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'} {#if type != 'viewonly'}
<li> <li>
@@ -380,8 +388,8 @@
{/if} {/if}
</ul> </ul>
</div> </div>
{:else if user && collection.shared_with && collection.shared_with.includes(user.uuid) && type != 'link'} {:else if isSharedMember && type != 'link'}
<div class="dropdown dropdown-end"> <div class="dropdown dropdown-end z-30">
<button <button
type="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" 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" /> <DotsHorizontal class="w-4 h-4" />
</button> </button>
<ul <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> <li>
<button <button

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,18 +4,14 @@
import { page } from '$app/stores'; import { page } from '$app/stores';
import CollectionCard from '$lib/components/cards/CollectionCard.svelte'; import CollectionCard from '$lib/components/cards/CollectionCard.svelte';
import CollectionModal from '$lib/components/CollectionModal.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 { t } from 'svelte-i18n';
import Plus from '~icons/mdi/plus'; import Plus from '~icons/mdi/plus';
import Filter from '~icons/mdi/filter-variant'; import Filter from '~icons/mdi/filter-variant';
import Sort from '~icons/mdi/sort'; import Sort from '~icons/mdi/sort';
import Archive from '~icons/mdi/archive'; import Archive from '~icons/mdi/archive';
import Share from '~icons/mdi/share-variant';
import CollectionIcon from '~icons/mdi/folder-multiple'; 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 { addToast } from '$lib/toasts';
import DeleteWarning from '$lib/components/DeleteWarning.svelte'; import DeleteWarning from '$lib/components/DeleteWarning.svelte';
@@ -28,7 +24,7 @@
let newType: string = ''; let newType: string = '';
let resultsPerPage: number = 25; let resultsPerPage: number = 25;
let isShowingCollectionModal: boolean = false; 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 next: string | null = data.props.next || null;
let previous: string | null = data.props.previous || null; let previous: string | null = data.props.previous || null;
@@ -39,35 +35,19 @@
let orderDirection = data.props.order_direction || 'asc'; let orderDirection = data.props.order_direction || 'asc';
let statusFilter = data.props.status || ''; let statusFilter = data.props.status || '';
let invites: CollectionInvite[] = data.props.invites || [];
let sidebarOpen = false; let sidebarOpen = false;
let collectionToEdit: Collection | null = null; let collectionToEdit: Collection | null = null;
$: currentCollections = $: mergedCollections = [...collections, ...sharedCollections].filter(
activeView === 'owned' (collection, index, list) => index === list.findIndex((entry) => entry.id === collection.id)
? collections );
: activeView === 'shared'
? sharedCollections
: activeView === 'archived'
? archivedCollections
: [];
$: currentCount = $: currentCollections = activeView === 'collections' ? mergedCollections : archivedCollections;
activeView === 'owned'
? collections.length
: activeView === 'shared'
? sharedCollections.length
: activeView === 'archived'
? archivedCollections.length
: activeView === 'invites'
? invites.length
: 0;
// Optionally, keep count in sync with collections only for owned view // Keep count in sync with collections view
$: { $: {
if (activeView === 'owned' && count !== collections.length) { if (activeView === 'collections' && count !== mergedCollections.length) {
count = collections.length; count = mergedCollections.length;
} }
} }
@@ -200,7 +180,7 @@
(collection) => collection.id !== duplicatedCollection.id (collection) => collection.id !== duplicatedCollection.id
); );
activeView = 'owned'; activeView = 'collections';
} }
async function editCollection(event: CustomEvent<SlimCollection>) { async function editCollection(event: CustomEvent<SlimCollection>) {
@@ -252,86 +232,9 @@
sidebarOpen = !sidebarOpen; sidebarOpen = !sidebarOpen;
} }
function switchView(view: 'owned' | 'shared' | 'archived' | 'invites') { function switchView(view: 'collections' | 'archived') {
activeView = view; 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> </script>
<svelte:head> <svelte:head>
@@ -406,25 +309,14 @@
<div class="flex border-b border-base-200 mt-4 overflow-x-auto"> <div class="flex border-b border-base-200 mt-4 overflow-x-auto">
<button <button
class="tab px-4 py-2 text-sm font-medium border-b-2 transition-colors whitespace-nowrap {activeView === 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-primary text-primary'
: 'border-transparent text-base-content/60 hover:text-base-content'}" : 'border-transparent text-base-content/60 hover:text-base-content'}"
on:click={() => switchView('owned')} on:click={() => switchView('collections')}
> >
<CollectionIcon class="w-4 h-4" /> <CollectionIcon class="w-4 h-4" />
<span class="hidden sm:inline">{$t('adventures.my_collections')}</span> <span class="hidden sm:inline">{$t('navbar.collections')}</span>
<div class="badge badge-sm badge-ghost">{collections.length}</div> <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 ===
'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>
</button> </button>
<button <button
class="tab px-4 py-2 text-sm font-medium border-b-2 transition-colors whitespace-nowrap {activeView === 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> <span class="hidden sm:inline">{$t('adventures.archived')}</span>
<div class="badge badge-sm badge-ghost">{archivedCollections.length}</div> <div class="badge badge-sm badge-ghost">{archivedCollections.length}</div>
</button> </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> </div>
</div> </div>
<!-- Main Content --> <!-- Main Content -->
<div class="container mx-auto px-6 py-8"> <div class="container mx-auto px-6 py-8">
{#if activeView === 'invites'} {#if currentCollections.length === 0}
<!-- 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}
<!-- Empty State for Collections --> <!-- Empty State for Collections -->
<div class="flex flex-col items-center justify-center py-16"> <div class="flex flex-col items-center justify-center py-16">
<div class="p-6 bg-base-200/50 rounded-2xl mb-6"> <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" /> <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} {:else}
<Archive class="w-16 h-16 text-base-content/30" /> <Archive class="w-16 h-16 text-base-content/30" />
{/if} {/if}
</div> </div>
<h3 class="text-xl font-semibold text-base-content/70 mb-2"> <h3 class="text-xl font-semibold text-base-content/70 mb-2">
{activeView === 'owned' {activeView === 'collections'
? $t('collection.no_collections_yet') ? $t('collection.no_collections_yet')
: activeView === 'shared'
? $t('collection.no_shared_collections')
: $t('collection.no_archived_collections')} : $t('collection.no_archived_collections')}
</h3> </h3>
<p class="text-base-content/50 text-center max-w-md"> <p class="text-base-content/50 text-center max-w-md">
{activeView === 'owned' {activeView === 'collections'
? $t('collection.create_first') ? $t('collection.create_first')
: activeView === 'shared'
? $t('collection.make_sure_public')
: $t('collection.archived_appear_here')} : $t('collection.archived_appear_here')}
</p> </p>
{#if activeView === 'owned'} {#if activeView === 'collections'}
<button <button
class="btn btn-primary btn-wide mt-6 gap-2" class="btn btn-primary btn-wide mt-6 gap-2"
on:click={() => { on:click={() => {
@@ -587,7 +391,7 @@
</div> </div>
<!-- Pagination --> <!-- Pagination -->
{#if activeView === 'owned' && (next || previous)} {#if activeView === 'collections' && (next || previous)}
<div class="flex justify-center mt-12"> <div class="flex justify-center mt-12">
<div class="join bg-base-100 shadow-lg rounded-2xl p-2"> <div class="join bg-base-100 shadow-lg rounded-2xl p-2">
{#each Array.from({ length: totalPages }, (_, i) => i + 1) as page} {#each Array.from({ length: totalPages }, (_, i) => i + 1) as page}
@@ -620,10 +424,8 @@
<h2 class="text-xl font-bold">{$t('adventures.filters_and_sort')}</h2> <h2 class="text-xl font-bold">{$t('adventures.filters_and_sort')}</h2>
</div> </div>
<!-- Only show sort options for collection views, not invites --> <!-- Status Filter -->
{#if activeView !== 'invites'} <div class="card bg-base-200/50 p-4 mb-4">
<!-- 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"> <h3 class="font-semibold text-lg mb-4 flex items-center gap-2">
<Filter class="w-5 h-5" /> <Filter class="w-5 h-5" />
{$t('adventures.status_filter')} {$t('adventures.status_filter')}
@@ -681,10 +483,10 @@
<span class="label-text">{$t('adventures.completed')}</span> <span class="label-text">{$t('adventures.completed')}</span>
</label> </label>
</div> </div>
</div> </div>
<!-- Sort Form - Updated to use URL navigation --> <!-- Sort Form - Updated to use URL navigation -->
<div class="card bg-base-200/50 p-4"> <div class="card bg-base-200/50 p-4">
<h3 class="font-semibold text-lg mb-4 flex items-center gap-2"> <h3 class="font-semibold text-lg mb-4 flex items-center gap-2">
<Sort class="w-5 h-5" /> <Sort class="w-5 h-5" />
{$t(`adventures.sort`)} {$t(`adventures.sort`)}
@@ -755,15 +557,14 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{/if}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Floating Action Button --> <!-- Floating Action Button -->
{#if activeView === 'owned'} {#if activeView === 'collections'}
<div class="fixed bottom-6 right-6 z-50"> <div class="fixed bottom-6 right-6 z-50">
<div class="dropdown dropdown-top dropdown-end"> <div class="dropdown dropdown-top dropdown-end">
<div <div