Files
minstrel/web/src/lib/components/wanted/FollowList.svelte
T
bvandeusenandClaude Opus 5.5 10d50dccbd docs: web comments say what the app does, not how it came to be (#6277)
Rewrites the web app's comments to rule 32. Task, milestone, rule and
spec ids are gone; regression stories are restated as the trap they
guard (public auth routes, the menu's stopPropagation, universal loads
under adapter-static, metric deltas with their margin). Flutter and
Tauri mentions now name the Android app or a native shell, operator
quotes and dates are dropped, and one library's counts no longer
appear outside test fixtures. Verified with a TypeScript-scanner token
comparison (template literals and regex literals re-scanned): only
comment text changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:42:12 -04:00

119 lines
4.4 KiB
Svelte

<script lang="ts">
import { plural } from '#lib/utils/plural.js';
import { Disc3 } from 'lucide-svelte';
import { useQueryClient } from '@tanstack/svelte-query';
import {
createFollowsQuery,
createReleaseProfilesQuery,
putFollow,
deleteFollow
} from '#lib/api/wanted.js';
import { errMessage } from '#lib/api/errors.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
import { relativeTime } from '#lib/utils/relativeTime.js';
import type { Follow } from '#lib/api/types.js';
// The Following tab: every artist whose new releases are
// wanted, each on a release-type profile saying which kinds of release.
const client = useQueryClient();
const followsStore = createFollowsQuery();
const follows = $derived($followsStore);
const profilesStore = createReleaseProfilesQuery();
const profiles = $derived($profilesStore.data?.profiles ?? []);
let busy = $state<string | null>(null);
async function refresh() {
await Promise.all([
client.invalidateQueries({ queryKey: ['adminWants'] }),
client.invalidateQueries({ queryKey: ['adminFollows'] })
]);
}
async function onProfile(f: Follow, profileId: string) {
busy = f.artist_mbid;
try {
await putFollow(f.artist_mbid, { artist_name: f.artist_name, profile_id: profileId });
await refresh();
} catch (e) {
pushToast(errMessage(e), 'error');
} finally {
busy = null;
}
}
async function onUnfollow(f: Follow) {
busy = f.artist_mbid;
try {
await deleteFollow(f.artist_mbid);
pushToast(`No longer following ${f.artist_name}. What was already wanted stays wanted.`);
await refresh();
} catch (e) {
pushToast(errMessage(e), 'error');
} finally {
busy = null;
}
}
function hideBroken(e: Event) {
(e.currentTarget as HTMLImageElement).style.display = 'none';
}
</script>
{#if follows.isPending}
<p class="text-text-secondary">Reading who's followed…</p>
{:else if follows.isError}
<p class="text-error-fg">Couldn't load the followed artists.</p>
{:else if (follows.data ?? []).length === 0}
<p class="text-text-secondary">No artists are followed. Follow one from its artist page.</p>
{:else}
<ul class="divide-y divide-border rounded-lg border border-border bg-surface">
{#each follows.data ?? [] as f (f.artist_mbid)}
<li class="flex flex-wrap items-center gap-3 p-3" data-testid="follow-row">
<div class="relative flex h-12 w-12 shrink-0 items-center justify-center overflow-hidden rounded-full bg-surface-hover" aria-hidden="true">
<Disc3 size={20} strokeWidth={1} class="text-text-muted" />
{#if f.image_url}
<img src={f.image_url} alt="" class="absolute inset-0 h-full w-full object-cover" loading="lazy" onerror={hideBroken} />
{/if}
</div>
<div class="min-w-0 flex-1 space-y-0.5">
<div class="truncate text-sm font-medium text-text-primary">
{#if f.artist_id}
<a href="/artists/{f.artist_id}" class="hover:underline">{f.artist_name}</a>
{:else}
{f.artist_name}
{/if}
</div>
<div class="text-xs text-text-secondary">
{plural(f.open_wants, 'album wanted', 'albums wanted')} ·
{f.checked_at ? `checked ${relativeTime(f.checked_at)}` : 'not checked yet'}
</div>
</div>
<label class="flex items-center gap-2 text-xs text-text-secondary">
<span class="sr-only">Profile for {f.artist_name}</span>
<select
value={f.profile_id}
disabled={busy === f.artist_mbid}
onchange={(e) => onProfile(f, (e.currentTarget as HTMLSelectElement).value)}
class="rounded-md border border-border bg-background px-2 py-1 text-sm text-text-primary focus:outline-hidden focus:ring-2 focus:ring-accent"
>
{#each profiles as p (p.id)}
<option value={p.id}>{p.name}</option>
{/each}
</select>
</label>
<button
type="button"
onclick={() => onUnfollow(f)}
disabled={busy === f.artist_mbid}
aria-label="Unfollow {f.artist_name}"
class="rounded-md border border-border px-2.5 py-1 text-xs text-text-secondary hover:bg-surface-hover hover:text-text-primary disabled:opacity-50"
>
Unfollow
</button>
</li>
{/each}
</ul>
{/if}