Add coauthor invitations tab with accept and reject
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
<script lang="ts">
|
||||
import type { SubmitFunction } from '@sveltejs/kit';
|
||||
import { flip } from 'svelte/animate';
|
||||
import { enhance } from '$app/forms';
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import Mail from '@lucide/svelte/icons/mail';
|
||||
import type { CoauthorInvitation } from '$lib/types';
|
||||
import { fmtDate } from '$lib/utils/format';
|
||||
import { collapse } from '$lib/utils/transitions';
|
||||
import Alert from '$lib/components/ui/primitives/Alert.svelte';
|
||||
import Button from '$lib/components/ui/primitives/Button.svelte';
|
||||
import EmptyState from '$lib/components/ui/primitives/EmptyState.svelte';
|
||||
import ListRow from '$lib/components/ui/media/ListRow.svelte';
|
||||
|
||||
interface Props {
|
||||
invitations: CoauthorInvitation[];
|
||||
}
|
||||
|
||||
let { invitations }: Props = $props();
|
||||
|
||||
let hiddenIds = $state<string[]>([]);
|
||||
let error = $state('');
|
||||
|
||||
const visible = $derived(invitations.filter((invitation) => !hiddenIds.includes(invitation.id)));
|
||||
|
||||
const respondSubmit: SubmitFunction = ({ formData }) => {
|
||||
const id = String(formData.get('invitationId'));
|
||||
hiddenIds = [...hiddenIds, id];
|
||||
error = '';
|
||||
return async ({ result, update }) => {
|
||||
if (result.type === 'success') {
|
||||
await update({ reset: false });
|
||||
hiddenIds = hiddenIds.filter((item) => item !== id);
|
||||
return;
|
||||
}
|
||||
hiddenIds = hiddenIds.filter((item) => item !== id);
|
||||
error =
|
||||
result.type === 'failure' && typeof result.data?.message === 'string'
|
||||
? result.data.message
|
||||
: 'No se pudo responder a la invitación.';
|
||||
await invalidateAll();
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if error}
|
||||
<Alert tone="danger" class="mt-4">{error}</Alert>
|
||||
{/if}
|
||||
|
||||
{#if visible.length > 0}
|
||||
<ul class="mt-4 flex flex-col">
|
||||
{#each visible as invitation, i (invitation.id)}
|
||||
<li
|
||||
animate:flip={{ duration: 200 }}
|
||||
out:collapse
|
||||
class="animate-enter"
|
||||
style="--i:{Math.min(i, 9)}"
|
||||
>
|
||||
<ListRow
|
||||
trackId={invitation.trackId}
|
||||
title={invitation.trackTitle}
|
||||
subtitle={invitation.artist}
|
||||
subtitleHref={invitation.ownerUserId}
|
||||
size="lg"
|
||||
class="flex-wrap sm:flex-nowrap"
|
||||
>
|
||||
{#snippet trailing()}
|
||||
<span class="ml-auto hidden text-xs text-fg-2 sm:block">
|
||||
{fmtDate(invitation.invitedAt)}
|
||||
</span>
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<form method="POST" action="?/reject" use:enhance={respondSubmit}>
|
||||
<input type="hidden" name="invitationId" value={invitation.id} />
|
||||
<Button type="submit" size="sm" variant="secondary">Rechazar</Button>
|
||||
</form>
|
||||
<form method="POST" action="?/accept" use:enhance={respondSubmit}>
|
||||
<input type="hidden" name="invitationId" value={invitation.id} />
|
||||
<Button type="submit" size="sm">Aceptar</Button>
|
||||
</form>
|
||||
</div>
|
||||
{/snippet}
|
||||
</ListRow>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<EmptyState
|
||||
icon={Mail}
|
||||
title="No tienes invitaciones"
|
||||
description="Cuando otro artista te invite a ser coautor de una canción, la verás aquí para aceptarla o rechazarla."
|
||||
/>
|
||||
{/if}
|
||||
@@ -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}
|
||||
<span
|
||||
class="ml-1.5 inline-grid min-w-5 place-items-center rounded-full px-1.5 text-xs tabular-nums {tab.active
|
||||
? 'bg-ink/15 text-ink'
|
||||
: 'bg-accent-tint text-accent-soft'}"
|
||||
aria-label="{tab.count} pendientes"
|
||||
>
|
||||
{tab.count}
|
||||
</span>
|
||||
{/if}
|
||||
</a>
|
||||
{/each}
|
||||
</nav>
|
||||
|
||||
@@ -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 })) };
|
||||
};
|
||||
@@ -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
|
||||
}))
|
||||
);
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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')
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import LibraryInvitationsSection from '$lib/components/music/library/LibraryInvitationsSection.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
<LibraryInvitationsSection invitations={data.invitations.items} />
|
||||
Reference in New Issue
Block a user