diff --git a/web-player/src/lib/components/music/library/LibraryInvitationsSection.svelte b/web-player/src/lib/components/music/library/LibraryInvitationsSection.svelte
new file mode 100644
index 0000000..cc81289
--- /dev/null
+++ b/web-player/src/lib/components/music/library/LibraryInvitationsSection.svelte
@@ -0,0 +1,92 @@
+
+
+{#if error}
+ {error}
+{/if}
+
+{#if visible.length > 0}
+
+ {#each visible as invitation, i (invitation.id)}
+ -
+
+ {#snippet trailing()}
+
+ {fmtDate(invitation.invitedAt)}
+
+
+
+
+
+ {/snippet}
+
+
+ {/each}
+
+{:else}
+
+{/if}
diff --git a/web-player/src/lib/components/ui/primitives/Tabs.svelte b/web-player/src/lib/components/ui/primitives/Tabs.svelte
index 681006a..3248908 100644
--- a/web-player/src/lib/components/ui/primitives/Tabs.svelte
+++ b/web-player/src/lib/components/ui/primitives/Tabs.svelte
@@ -3,6 +3,7 @@
href: string;
label: string;
active: boolean;
+ count?: number;
}
interface Props {
@@ -51,6 +52,16 @@
: 'text-fg-2 hover:text-fg'}"
>
{tab.label}
+ {#if tab.count}
+
+ {tab.count}
+
+ {/if}
{/each}
diff --git a/web-player/src/routes/(app)/library/+layout.server.ts b/web-player/src/routes/(app)/library/+layout.server.ts
new file mode 100644
index 0000000..fe20059
--- /dev/null
+++ b/web-player/src/routes/(app)/library/+layout.server.ts
@@ -0,0 +1,9 @@
+import type { LayoutServerLoad } from './$types';
+import { apiFor } from '$lib/server/api';
+import { requireUser } from '$lib/server/guards';
+import { loadInvitationCount } from '$lib/server/coauthors';
+
+export const load: LayoutServerLoad = async ({ locals, url, fetch }) => {
+ requireUser(locals, url);
+ return { invitationCount: await loadInvitationCount(apiFor({ fetch, locals })) };
+};
diff --git a/web-player/src/routes/(app)/library/+layout.svelte b/web-player/src/routes/(app)/library/+layout.svelte
index 897784d..b100151 100644
--- a/web-player/src/routes/(app)/library/+layout.svelte
+++ b/web-player/src/routes/(app)/library/+layout.svelte
@@ -24,7 +24,7 @@
import PageHeader from '$lib/components/ui/layout/PageHeader.svelte';
import Tabs from '$lib/components/ui/primitives/Tabs.svelte';
- let { children } = $props();
+ let { data, children } = $props();
const pathname = $derived(page.url.pathname.replace(/\/$/, ''));
const current = $derived(
@@ -35,7 +35,12 @@
) ?? SECTIONS[0]
);
const tabs = $derived(
- SECTIONS.map(({ href, label }) => ({ href, label, active: href === current.href }))
+ SECTIONS.map(({ href, label }) => ({
+ href,
+ label,
+ active: href === current.href,
+ count: href === '/library/invitations' ? data.invitationCount : undefined
+ }))
);
diff --git a/web-player/src/routes/(app)/library/invitations/+page.server.ts b/web-player/src/routes/(app)/library/invitations/+page.server.ts
new file mode 100644
index 0000000..cfe3d6d
--- /dev/null
+++ b/web-player/src/routes/(app)/library/invitations/+page.server.ts
@@ -0,0 +1,25 @@
+import type { PageServerLoad, Actions } from './$types';
+import { fail } from '@sveltejs/kit';
+import { apiFor } from '$lib/server/api';
+import { requireUser } from '$lib/server/guards';
+import { formString } from '$lib/server/forms/fields';
+import { tagged } from '$lib/server/actions/tagged';
+import { loadInvitations, respondToInvitation } from '$lib/server/coauthors';
+
+export const load: PageServerLoad = async ({ locals, url, fetch }) => {
+ requireUser(locals, url);
+ return { invitations: await loadInvitations(apiFor({ fetch, locals })) };
+};
+
+function respond(decision: 'accept' | 'reject') {
+ return tagged(decision, async ({ api, form }) => {
+ const invitationId = formString(form, 'invitationId');
+ if (!invitationId) return fail(400, { message: 'Falta la invitación.' });
+ return respondToInvitation(api, invitationId, decision);
+ });
+}
+
+export const actions: Actions = {
+ accept: respond('accept'),
+ reject: respond('reject')
+};
diff --git a/web-player/src/routes/(app)/library/invitations/+page.svelte b/web-player/src/routes/(app)/library/invitations/+page.svelte
new file mode 100644
index 0000000..bc4e00c
--- /dev/null
+++ b/web-player/src/routes/(app)/library/invitations/+page.svelte
@@ -0,0 +1,7 @@
+
+
+