docs: Changelog, Release-Notes und DEVLOG für 0.3.0

This commit is contained in:
Florian Wilde
2026-09-01 19:24:53 +02:00
parent a3c74c157b
commit e2565b600f
4 changed files with 122 additions and 139 deletions
-137
View File
@@ -1,137 +0,0 @@
"use client";
import { useState } from "react";
import { clsx } from "clsx";
/**
* Staffel- und Episodenliste.
*
* Staffeln sind eingeklappt, damit eine Serie mit zwanzig Staffeln die Seite
* nicht sprengt; die erste reguläre Staffel steht offen. In Etappe 3 kommen
* hier die Häkchen für den Fortschritt dazu.
*/
export type EpisodeData = {
id: string;
episodeNumber: number;
name: string;
overview: string | null;
airDate: string | null;
runtime: number | null;
};
export type SeasonData = {
id: string;
seasonNumber: number;
name: string;
overview: string | null;
airDate: string | null;
episodes: EpisodeData[];
};
function formatDate(value: string | null): string | null {
if (!value) return null;
return new Date(value).toLocaleDateString("de-DE", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
}
export function SeasonList({ seasons }: { seasons: SeasonData[] }) {
const firstRegular = seasons.find((season) => season.seasonNumber > 0) ?? seasons[0];
const [open, setOpen] = useState<string[]>(firstRegular ? [firstRegular.id] : []);
function toggle(id: string) {
setOpen((current) =>
current.includes(id) ? current.filter((entry) => entry !== id) : [...current, id],
);
}
if (seasons.length === 0) {
return <p className="text-sm text-[var(--fl-faint)]">Zu dieser Serie liegen keine Staffeln vor.</p>;
}
return (
<div className="space-y-2">
{seasons.map((season) => {
const isOpen = open.includes(season.id);
const air = formatDate(season.airDate);
return (
<section key={season.id} className="fl-card overflow-hidden">
<h3>
<button
type="button"
onClick={() => toggle(season.id)}
aria-expanded={isOpen}
className="flex w-full items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-[var(--fl-surface-2)]"
>
<Chevron open={isOpen} />
<span className="flex-1">
<span className="font-medium">{season.name}</span>
<span className="ml-2 text-sm text-[var(--fl-faint)]">
{season.episodes.length}{" "}
{season.episodes.length === 1 ? "Episode" : "Episoden"}
{air ? ` · ab ${air}` : ""}
</span>
</span>
</button>
</h3>
{isOpen && (
<ol className="border-t border-[var(--fl-border)]">
{season.episodes.map((episode) => (
<li
key={episode.id}
className="flex gap-3 border-b border-[var(--fl-border)] px-4 py-2.5 last:border-b-0"
>
<span className="w-14 shrink-0 pt-0.5 font-mono text-xs text-[var(--fl-faint)]">
S{String(season.seasonNumber).padStart(2, "0")}E
{String(episode.episodeNumber).padStart(2, "0")}
</span>
<span className="min-w-0 flex-1">
<span className="block text-sm font-medium">{episode.name}</span>
{episode.overview && (
<span className="mt-0.5 line-clamp-2 block text-xs text-[var(--fl-faint)]">
{episode.overview}
</span>
)}
</span>
<span className="shrink-0 pt-0.5 text-right text-xs text-[var(--fl-faint)]">
{formatDate(episode.airDate) ?? "—"}
{episode.runtime ? <span className="block">{episode.runtime} min</span> : null}
</span>
</li>
))}
{season.episodes.length === 0 && (
<li className="px-4 py-3 text-sm text-[var(--fl-faint)]">
Für diese Staffel liegen noch keine Episoden vor.
</li>
)}
</ol>
)}
</section>
);
})}
</div>
);
}
function Chevron({ open }: { open: boolean }) {
return (
<svg
viewBox="0 0 24 24"
className={clsx(
"h-4 w-4 shrink-0 text-[var(--fl-muted)] transition-transform duration-200",
open && "rotate-90",
)}
fill="none"
stroke="currentColor"
strokeWidth="2"
aria-hidden
>
<path d="m9 6 6 6-6 6" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}