feat(web/m7-365): /library/history page with day grouping + infinite scroll

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-04 06:53:37 -04:00
co-authored by Claude Sonnet 4.6
parent da39ff847b
commit ecf4ed86f5
2 changed files with 164 additions and 0 deletions
@@ -0,0 +1,47 @@
<script lang="ts">
import { createHistoryQuery, type HistoryEvent } from '$lib/api/history';
import { groupByDay } from '$lib/utils/dayGroup';
import { pageTitle } from '$lib/branding';
import HistoryRow from '$lib/components/HistoryRow.svelte';
import InfiniteScrollSentinel from '$lib/components/InfiniteScrollSentinel.svelte';
import LibrarySkeleton from '$lib/components/LibrarySkeleton.svelte';
import ApiErrorBanner from '$lib/components/ApiErrorBanner.svelte';
const query = createHistoryQuery();
const flatEvents = $derived(
($query.data?.pages ?? []).flatMap((p) => p.events) as HistoryEvent[]
);
const grouped = $derived(groupByDay(flatEvents, new Date()));
</script>
<svelte:head><title>{pageTitle('Library · History')}</title></svelte:head>
<div class="space-y-6">
<h1 class="text-2xl font-semibold">History</h1>
{#if $query.isPending}
<LibrarySkeleton variant="list" count={12} />
{:else if $query.isError}
<ApiErrorBanner error={$query.error} onRetry={() => $query.refetch()} />
{:else if flatEvents.length === 0}
<p class="p-8 text-center text-text-secondary">No listening history yet.</p>
{:else}
{#each grouped as group (group.label)}
<section>
<h2 class="sticky top-0 mb-2 bg-background py-1 text-lg font-semibold text-text-secondary">
{group.label}
</h2>
<div class="divide-y divide-border">
{#each group.events as event (event.id)}
<HistoryRow {event} />
{/each}
</div>
</section>
{/each}
{#if $query.hasNextPage && !$query.isFetchingNextPage}
<InfiniteScrollSentinel onIntersect={() => $query.fetchNextPage()} />
{/if}
{/if}
</div>