api: add Person and StreamingService contexts

This commit is contained in:
Cron1cle
2025-10-07 20:08:51 +02:00
parent de2778d6db
commit 2dacb9fa80
12 changed files with 390 additions and 111 deletions

130
contexts/PersonContext.tsx Normal file
View File

@@ -0,0 +1,130 @@
import React, {
createContext,
useCallback,
useContext,
useState,
ReactNode,
} from "react";
import { getPersonHistory, PersonHistoryEntry } from "@/apis/personApi";
type PersonAppearances = {
raw: PersonHistoryEntry[];
byShow: Record<number, number[]>;
showIds: number[];
};
type PersonContextType = {
getPersonAppearances: (personId: number) => Promise<PersonAppearances>;
getShowIds: (personId: number) => Promise<number[]>;
getSeasonsForShow: (personId: number, showId: number) => Promise<number[]>;
isLoading: (personId: number) => boolean;
getError: (personId: number) => string | null;
invalidatePerson: (personId: number) => void;
};
const PersonContext = createContext<PersonContextType | null>(null);
export const PersonProvider = ({ children }: { children: ReactNode }) => {
const [cache, setCache] = useState<Record<number, PersonAppearances>>({});
const [loading, setLoading] = useState<Record<number, boolean>>({});
const [errors, setErrors] = useState<Record<number, string | null>>({});
const buildAppearances = (
entries: PersonHistoryEntry[]
): PersonAppearances => {
const byShow: Record<number, Set<number>> = {};
for (const e of entries) {
if (!byShow[e.showId]) byShow[e.showId] = new Set();
byShow[e.showId].add(e.seasonNumber);
}
const byShowSorted: Record<number, number[]> = Object.fromEntries(
Object.entries(byShow).map(([showId, seasonsSet]) => [
Number(showId),
Array.from(seasonsSet).sort((a, b) => a - b),
])
);
return {
raw: entries,
byShow: byShowSorted,
showIds: Object.keys(byShowSorted)
.map(Number)
.sort((a, b) => a - b),
};
};
const fetchAndCache = useCallback(async (personId: number) => {
setLoading((l) => ({ ...l, [personId]: true }));
setErrors((e) => ({ ...e, [personId]: null }));
try {
const data = await getPersonHistory(personId);
const appearances = buildAppearances(data);
setCache((c) => ({ ...c, [personId]: appearances }));
return appearances;
} catch (e: any) {
setErrors((err) => ({
...err,
[personId]: e?.message || "Fehler beim Laden",
}));
return { raw: [], byShow: {}, showIds: [] };
} finally {
setLoading((l) => ({ ...l, [personId]: false }));
}
}, []);
const getPersonAppearances = useCallback(
async (personId: number) => {
if (cache[personId]) return cache[personId];
return await fetchAndCache(personId);
},
[cache, fetchAndCache]
);
const getShowIds = useCallback(
async (personId: number) => {
const app = await getPersonAppearances(personId);
return app.showIds;
},
[getPersonAppearances]
);
const getSeasonsForShow = useCallback(
async (personId: number, showId: number) => {
const app = await getPersonAppearances(personId);
return app.byShow[showId] || [];
},
[getPersonAppearances]
);
const isLoading = (personId: number) => !!loading[personId];
const getError = (personId: number) => errors[personId] || null;
const invalidatePerson = (personId: number) => {
setCache((c) => {
const copy = { ...c };
delete copy[personId];
return copy;
});
};
return (
<PersonContext.Provider
value={{
getPersonAppearances,
getShowIds,
getSeasonsForShow,
isLoading,
getError,
invalidatePerson,
}}
>
{children}
</PersonContext.Provider>
);
};
export const usePersonContext = () => {
const ctx = useContext(PersonContext);
if (!ctx)
throw new Error("usePersonContext must be used within PersonProvider");
return ctx;
};

View File

@@ -1,5 +1,11 @@
import { getSeason, SeasonParticipant } from "@/apis/seasonApi";
import React, { createContext, useContext, useState, useCallback } from "react";
import React, {
createContext,
useContext,
useState,
useCallback,
ReactNode,
} from "react";
type SeasonContextType = {
fetchSeasonParticipants: (
@@ -11,11 +17,10 @@ type SeasonContextType = {
const SeasonContext = createContext<SeasonContextType | null>(null);
export const SeasonProvider = ({ children }: { children: React.ReactNode }) => {
export const SeasonProvider = ({ children }: { children: ReactNode }) => {
const [seasonCache, setSeasonCache] = useState<
Record<string, SeasonParticipant[]>
>({});
const [seasonCountCache, setSeasonCountCache] = useState<
Record<number, number>
>({});
@@ -24,7 +29,6 @@ export const SeasonProvider = ({ children }: { children: React.ReactNode }) => {
async (showId: number, seasonNumber: number) => {
const key = `${showId}-${seasonNumber}`;
if (seasonCache[key]) return seasonCache[key];
try {
const season = await getSeason(showId, seasonNumber);
const participants = season?.participants ?? [];
@@ -67,8 +71,8 @@ export const SeasonProvider = ({ children }: { children: React.ReactNode }) => {
};
export const useSeasonContext = () => {
const context = useContext(SeasonContext);
if (!context)
const ctx = useContext(SeasonContext);
if (!ctx)
throw new Error("useSeasonContext must be used within a SeasonProvider");
return context;
return ctx;
};

View File

@@ -0,0 +1,59 @@
import {
getStreamingImages,
StreamingServiceRaw,
} from "@/apis/streamingServiceApi";
import { createContext, useContext } from "react";
import React from "react";
type StreamingServiceContextType = {
streamingServices: Record<string, string>;
loading: boolean;
error: string | null;
};
const StreamingServiceContext =
createContext<StreamingServiceContextType | null>(null);
export const StreamingServiceProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const [streamingServices, setStreamingServices] = React.useState<
Record<string, string>
>({});
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState<string | null>(null);
React.useEffect(() => {
(async () => {
try {
const data: StreamingServiceRaw[] = await getStreamingImages();
const mapped = Object.fromEntries(data.map((s) => [s.key, s.value]));
setStreamingServices(mapped);
} catch {
setError("Failed to fetch streaming services");
} finally {
setLoading(false);
}
})();
}, []);
return (
<StreamingServiceContext.Provider
value={{ streamingServices, loading, error }}
>
{children}
</StreamingServiceContext.Provider>
);
};
export const useStreamingServiceContext = () => {
const ctx = useContext(StreamingServiceContext);
if (!ctx)
throw new Error(
"useStreamingServiceContext must be used within a StreamingServiceProvider"
);
return ctx;
};