Add coauthor invitations tab with accept and reject

This commit is contained in:
2026-09-25 02:22:46 +02:00
parent 058a902a81
commit dddb57c98b
6 changed files with 151 additions and 2 deletions
@@ -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>