feat: Suchseite und Titel-Detailseite
Die Suchanfrage steht in der URL: Treffer sind teilbar, der Zurück-Knopf funktioniert, und die Suche selbst bleibt eine Server-Komponente. Posterkacheln zeigen bei fehlendem Bild die Bildmarke über dem Signaturverlauf. Die Rückfalldarstellung liegt dauerhaft unter dem Bild — ein onError-Handler hilft nicht, wenn das Bild schon vor der Hydration scheitert, und der Browser zeigt dann sein kaputtes Bildsymbol.
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import { posterUrl } from "@/lib/images";
|
||||
|
||||
/**
|
||||
* Streamingdienste als Badges.
|
||||
*
|
||||
* Farbe und Logo kommen aus dem Katalog; Dienste ohne hinterlegte Akzentfarbe
|
||||
* bekommen einen neutralen Ton, damit die Reihe trotzdem ruhig aussieht.
|
||||
*/
|
||||
|
||||
export type ProviderBadgeData = {
|
||||
id: string;
|
||||
name: string;
|
||||
logoPath: string | null;
|
||||
color: string | null;
|
||||
};
|
||||
|
||||
const KIND_LABEL = {
|
||||
FLATRATE: "Im Abo",
|
||||
FREE: "Kostenlos",
|
||||
ADS: "Mit Werbung",
|
||||
RENT: "Leihen",
|
||||
BUY: "Kaufen",
|
||||
} as const;
|
||||
|
||||
export type ProviderKindKey = keyof typeof KIND_LABEL;
|
||||
|
||||
export function ProviderBadge({ provider }: { provider: ProviderBadgeData }) {
|
||||
const logo = posterUrl(provider.logoPath, "w92");
|
||||
const accent = provider.color ?? "var(--fl-border-strong)";
|
||||
|
||||
return (
|
||||
<span
|
||||
className="inline-flex items-center gap-1.5 rounded-full border py-1 pl-1 pr-3 text-xs font-medium"
|
||||
style={{
|
||||
borderColor: `color-mix(in oklab, ${accent} 55%, transparent)`,
|
||||
backgroundColor: `color-mix(in oklab, ${accent} 14%, transparent)`,
|
||||
}}
|
||||
>
|
||||
{/* Farbpunkt liegt darunter; ein Logo, das nicht lädt, fällt in sich
|
||||
zusammen und gibt ihn wieder frei. */}
|
||||
<span className="relative h-5 w-5 shrink-0">
|
||||
<span
|
||||
aria-hidden
|
||||
className="absolute inset-0 rounded-full"
|
||||
style={{ backgroundColor: accent }}
|
||||
/>
|
||||
{logo && (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={logo}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="absolute inset-0 h-5 w-5 rounded-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
<span className="text-[var(--fl-text)]">{provider.name}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/** Alle Verfügbarkeiten eines Titels, nach Art gruppiert. */
|
||||
export function ProviderList({
|
||||
groups,
|
||||
region,
|
||||
}: {
|
||||
groups: Partial<Record<ProviderKindKey, ProviderBadgeData[]>>;
|
||||
region: string;
|
||||
}) {
|
||||
const order: ProviderKindKey[] = ["FLATRATE", "FREE", "ADS", "RENT", "BUY"];
|
||||
const present = order.filter((kind) => (groups[kind]?.length ?? 0) > 0);
|
||||
|
||||
if (present.length === 0) {
|
||||
return (
|
||||
<p className="text-sm text-[var(--fl-faint)]">
|
||||
Für die Region {region} meldet TMDB derzeit keinen Streamingdienst.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{present.map((kind) => (
|
||||
<div key={kind} className="flex flex-wrap items-center gap-2">
|
||||
<span className="w-24 shrink-0 text-xs font-semibold uppercase tracking-wide text-[var(--fl-faint)]">
|
||||
{KIND_LABEL[kind]}
|
||||
</span>
|
||||
{groups[kind]?.map((provider) => (
|
||||
<ProviderBadge key={`${kind}-${provider.id}`} provider={provider} />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user