First working POC
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
<script lang="ts">
|
||||
import type { Person, PersonType } from "$lib/api/types";
|
||||
import { auth } from "$lib/stores/auth";
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
interface Props {
|
||||
people: Person[];
|
||||
title?: string;
|
||||
}
|
||||
|
||||
let { people, title = "Cast & Crew" }: Props = $props();
|
||||
|
||||
// Group people by type
|
||||
const groupedPeople = $derived.by(() => {
|
||||
const groups: Record<string, Person[]> = {
|
||||
Actor: [],
|
||||
Director: [],
|
||||
Writer: [],
|
||||
Producer: [],
|
||||
Composer: [],
|
||||
Other: [],
|
||||
};
|
||||
|
||||
for (const person of people) {
|
||||
// Skip people without valid ID (can occur if API response is incomplete)
|
||||
if (!person.id || person.id.trim() === "") {
|
||||
continue;
|
||||
}
|
||||
const type = person.type;
|
||||
if (type in groups) {
|
||||
groups[type].push(person);
|
||||
} else {
|
||||
groups.Other.push(person);
|
||||
}
|
||||
}
|
||||
|
||||
return groups;
|
||||
});
|
||||
|
||||
// Order: Actors first, then Directors, Writers, etc.
|
||||
const orderedTypes = ["Actor", "Director", "Writer", "Producer", "Composer", "Other"] as const;
|
||||
|
||||
// Get display name for type
|
||||
function getTypeName(type: string): string {
|
||||
switch (type) {
|
||||
case "Actor":
|
||||
return "Cast";
|
||||
case "Director":
|
||||
return "Directors";
|
||||
case "Writer":
|
||||
return "Writers";
|
||||
case "Producer":
|
||||
return "Producers";
|
||||
case "Composer":
|
||||
return "Composers";
|
||||
default:
|
||||
return "Other";
|
||||
}
|
||||
}
|
||||
|
||||
function getPersonImageUrl(person: Person): string {
|
||||
try {
|
||||
const repo = auth.getRepository();
|
||||
return repo.getImageUrl(person.id, "Primary", {
|
||||
maxWidth: 200,
|
||||
tag: person.primaryImageTag,
|
||||
});
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function handlePersonClick(person: Person) {
|
||||
goto(`/library/${person.id}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="space-y-6">
|
||||
<h2 class="text-xl font-semibold text-white">{title}</h2>
|
||||
|
||||
{#each orderedTypes as type}
|
||||
{#if groupedPeople[type]?.length > 0}
|
||||
<div class="space-y-3">
|
||||
<h3 class="text-sm font-medium text-gray-400 uppercase tracking-wide">
|
||||
{getTypeName(type)}
|
||||
</h3>
|
||||
|
||||
<div class="flex gap-4 overflow-x-auto pb-2 scrollbar-thin scrollbar-thumb-gray-700">
|
||||
{#each groupedPeople[type] as person (person.id)}
|
||||
<button
|
||||
type="button"
|
||||
class="flex-shrink-0 w-24 group text-left"
|
||||
onclick={() => handlePersonClick(person)}
|
||||
>
|
||||
<!-- Person image -->
|
||||
<div class="w-24 h-24 rounded-full overflow-hidden bg-[var(--color-surface)] mb-2">
|
||||
{#if person.primaryImageTag}
|
||||
<img
|
||||
src={getPersonImageUrl(person)}
|
||||
alt={person.name}
|
||||
class="w-full h-full object-cover group-hover:scale-110 transition-transform"
|
||||
loading="lazy"
|
||||
/>
|
||||
{:else}
|
||||
<div class="w-full h-full flex items-center justify-center text-gray-500">
|
||||
<svg class="w-10 h-10" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Name and role -->
|
||||
<p class="text-sm font-medium text-white truncate group-hover:text-[var(--color-jellyfin)] transition-colors">
|
||||
{person.name}
|
||||
</p>
|
||||
{#if person.role}
|
||||
<p class="text-xs text-gray-400 truncate">
|
||||
{person.role}
|
||||
</p>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</section>
|
||||
Reference in New Issue
Block a user