api: add Person and StreamingService contexts
This commit is contained in:
130
contexts/PersonContext.tsx
Normal file
130
contexts/PersonContext.tsx
Normal 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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
59
contexts/StreamingServiceContext.tsx
Normal file
59
contexts/StreamingServiceContext.tsx
Normal 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;
|
||||
};
|
||||
Reference in New Issue
Block a user