jellytau/src/lib/components/library/CrewLinks.svelte
Duncan Tourolle 14e9d7e03a
Some checks failed
Traceability Validation / Check Requirement Traces (push) Failing after 21s
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 13m21s
🏗️ Build and Test JellyTau / Build Android APK (push) Failing after 2m14s
E frontend reconciliation (2/2): align consumers to generated bindings; svelte-check clean
Reconcile the ~50 frontend files that consumed the old hand-written types against
the stricter generated bindings (141 -> 0 svelte-check errors):

- Nullable strictness: null-coalesce/guard at consumer sites; widen shared helpers
  (CachedImage.tag, ticksToSeconds, formatDuration, getSessionIcon, session store
  send*/transferToRemote) to accept null.
- Field-name fixes exposed by codegen: userData.played -> isPlayed; remote
  NowPlayingItem uses album (not albumName); ArtistItem id/name.
- player.ts: normalize remote NowPlayingItem -> MediaItem in mergedMedia.
- ArtistItem now serializes camelCase (PascalCase alias retained for Jellyfin
  deserialize); update its serialize test.
- Update downloads.test.ts to the bundled { request } shape; complete the
  sessions.test.ts NowPlayingItem mock.

Frontend: svelte-check 0 errors, vitest 448 passing. Backend: 387 passing.
2026-06-20 20:49:20 +02:00

73 lines
2.0 KiB
Svelte

<!-- TRACES: UR-035 | JA-029 | DR-040 -->
<script lang="ts">
import { goto } from "$app/navigation";
import type { Person } from "$lib/api/types";
interface Props {
people: Person[];
roleFilter: string[]; // e.g., ["Director", "Writer", "Producer"]
label?: string; // e.g., "Directed by", "Written by"
maxShow?: number; // Default: 3
}
let {
people,
roleFilter,
label,
maxShow = 3
}: Props = $props();
// Filter and limit people by role
const filteredPeople = $derived(
people
.filter(p => roleFilter.includes(p.type || "") && p.id && p.id.trim() !== "")
.slice(0, maxShow)
);
const totalMatching = $derived(
people.filter(p => roleFilter.includes(p.type || "") && p.id && p.id.trim() !== "").length
);
function handlePersonClick(personId: string, e: MouseEvent) {
e.preventDefault();
goto(`/library/${personId}`);
}
// Generate label if not provided
const displayLabel = $derived.by(() => {
if (label) return label;
if (roleFilter.length === 1) {
const role = roleFilter[0];
if (role === "Director") return "Directed by";
if (role === "Writer") return "Written by";
if (role === "Producer") return "Produced by";
if (role === "Composer") return "Music by";
return `${role}:`;
}
return "Credits:";
});
</script>
{#if filteredPeople.length > 0}
<div class="text-sm text-gray-400 flex flex-wrap items-baseline gap-2">
<span class="text-gray-500">{displayLabel}</span>
<div class="flex flex-wrap gap-1">
{#each filteredPeople as person, index (person.id)}
<button
onclick={(e) => handlePersonClick(person.id ?? "", e)}
class="text-[var(--color-jellyfin)] hover:underline"
>
{person.name}
</button>
{#if index < filteredPeople.length - 1}
<span class="text-gray-500">,</span>
{/if}
{/each}
{#if totalMatching > maxShow}
<span class="text-gray-500">+{totalMatching - maxShow} more</span>
{/if}
</div>
</div>
{/if}