api: add Person and StreamingService contexts
This commit is contained in:
40
apis/personApi.ts
Normal file
40
apis/personApi.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
export type PersonHistorySeasonRaw = {
|
||||||
|
seasonId: number;
|
||||||
|
show: number;
|
||||||
|
seasonNumber: number;
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string | null;
|
||||||
|
seasonParticipants: any[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PersonHistoryEntry = {
|
||||||
|
showId: number;
|
||||||
|
seasonId: number;
|
||||||
|
seasonNumber: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PERSON_API_BASE = "http://45.157.177.99:8080/persons";
|
||||||
|
|
||||||
|
export async function getPersonHistory(
|
||||||
|
personId: number
|
||||||
|
): Promise<PersonHistoryEntry[]> {
|
||||||
|
const url = `${PERSON_API_BASE}/${personId}/history`;
|
||||||
|
try {
|
||||||
|
console.log("[getPersonHistory] Fetch:", url);
|
||||||
|
const res = await fetch(url);
|
||||||
|
if (!res.ok) throw new Error("History fetch failed " + res.status);
|
||||||
|
const data: unknown = await res.json();
|
||||||
|
if (!Array.isArray(data)) {
|
||||||
|
console.warn("History expected array, got:", data);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return (data as PersonHistorySeasonRaw[]).map((s) => ({
|
||||||
|
showId: s.show,
|
||||||
|
seasonId: s.seasonId,
|
||||||
|
seasonNumber: s.seasonNumber,
|
||||||
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
console.error("getPersonHistory error:", e);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,14 +41,13 @@ export async function getSeason(
|
|||||||
showId: number,
|
showId: number,
|
||||||
seasonNumber: number
|
seasonNumber: number
|
||||||
): Promise<Season | null> {
|
): Promise<Season | null> {
|
||||||
// WICHTIG: trailing Slash entfernt
|
|
||||||
const url = `${SEASON_BASE_URL}/${showId}/seasons/${seasonNumber}`;
|
const url = `${SEASON_BASE_URL}/${showId}/seasons/${seasonNumber}`;
|
||||||
try {
|
try {
|
||||||
console.log("[getSeason] Fetch:", url);
|
console.log("[getSeason] Fetch:", url);
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
console.log("[getSeason] Status:", res.status);
|
console.log("[getSeason] Status:", res.status);
|
||||||
if (res.status === 404) return null;
|
if (res.status === 404) return null;
|
||||||
if (!res.ok) throw new Error(`Season fetch failed: ${res.status}`);
|
if (!res.ok) throw new Error("Season fetch failed " + res.status);
|
||||||
const raw: RawSeason = await res.json();
|
const raw: RawSeason = await res.json();
|
||||||
const participants: SeasonParticipant[] = raw.seasonParticipants.map(
|
const participants: SeasonParticipant[] = raw.seasonParticipants.map(
|
||||||
(p) => ({
|
(p) => ({
|
||||||
|
|||||||
29
apis/streamingServiceApi.ts
Normal file
29
apis/streamingServiceApi.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
export type StreamingServiceRaw = {
|
||||||
|
id: number;
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const STREAMING_SERVICE_API_URL = "http://45.157.177.99:8080/config";
|
||||||
|
|
||||||
|
export async function getStreamingImages(): Promise<StreamingServiceRaw[]> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(STREAMING_SERVICE_API_URL);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Network response was not ok");
|
||||||
|
}
|
||||||
|
const data: unknown = await response.json();
|
||||||
|
if (!Array.isArray(data)) {
|
||||||
|
console.warn("Expected array, got:", data);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return (data as StreamingServiceRaw[]).map((s) => ({
|
||||||
|
id: s.id,
|
||||||
|
key: s.key,
|
||||||
|
value: s.value,
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Fetch error:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import styles from "@/app/tabStyles/indexStyles";
|
import styles from "@/app/tabStyles/indexStyles";
|
||||||
import ShowCard from "@/components/ui/ShowCard";
|
import ShowCard from "@/components/ui/ShowCard";
|
||||||
import { useShowContext } from "@/contexts/ShowContext";
|
import { useShowContext } from "@/contexts/ShowContext";
|
||||||
|
import { useStreamingServiceContext } from "@/contexts/StreamingServiceContext";
|
||||||
import { router } from "expo-router";
|
import { router } from "expo-router";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { ActivityIndicator, Text, View } from "react-native";
|
import { ActivityIndicator, Text, View } from "react-native";
|
||||||
@@ -11,6 +12,7 @@ import {
|
|||||||
|
|
||||||
export default function HomeScreen() {
|
export default function HomeScreen() {
|
||||||
const { shows, error, loading } = useShowContext();
|
const { shows, error, loading } = useShowContext();
|
||||||
|
const { streamingServices } = useStreamingServiceContext();
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -47,6 +49,11 @@ export default function HomeScreen() {
|
|||||||
<ScrollView contentContainerStyle={{ paddingBottom: 30 }}>
|
<ScrollView contentContainerStyle={{ paddingBottom: 30 }}>
|
||||||
{shows.map((show) => {
|
{shows.map((show) => {
|
||||||
const showLiveBadge = show.running;
|
const showLiveBadge = show.running;
|
||||||
|
const streamingService =
|
||||||
|
streamingServices[
|
||||||
|
`assets.images.streamingServices.${show.streamingService.toLowerCase()}`
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ShowCard
|
<ShowCard
|
||||||
key={show.id}
|
key={show.id}
|
||||||
@@ -66,7 +73,7 @@ export default function HomeScreen() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
imageUri={show.bannerUri}
|
imageUri={show.bannerUri}
|
||||||
streamingServiceUri={show.streamingService}
|
streamingServiceUri={streamingService}
|
||||||
genres={show.genres}
|
genres={show.genres}
|
||||||
{...(showLiveBadge
|
{...(showLiveBadge
|
||||||
? {
|
? {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { ShowProvider } from "@/contexts/ShowContext";
|
import { ShowProvider } from "@/contexts/ShowContext";
|
||||||
import { SeasonProvider } from "@/contexts/SeasonContext";
|
import { SeasonProvider } from "@/contexts/SeasonContext";
|
||||||
|
import { StreamingServiceProvider } from "@/contexts/StreamingServiceContext";
|
||||||
|
import { PersonProvider } from "@/contexts/PersonContext";
|
||||||
import { Stack } from "expo-router";
|
import { Stack } from "expo-router";
|
||||||
import "react-native-reanimated";
|
import "react-native-reanimated";
|
||||||
|
|
||||||
@@ -7,6 +9,8 @@ export default function RootLayout() {
|
|||||||
return (
|
return (
|
||||||
<ShowProvider>
|
<ShowProvider>
|
||||||
<SeasonProvider>
|
<SeasonProvider>
|
||||||
|
<StreamingServiceProvider>
|
||||||
|
<PersonProvider>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
@@ -23,6 +27,8 @@ export default function RootLayout() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
</PersonProvider>
|
||||||
|
</StreamingServiceProvider>
|
||||||
</SeasonProvider>
|
</SeasonProvider>
|
||||||
</ShowProvider>
|
</ShowProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,80 +1,67 @@
|
|||||||
import { View, Image, Text, TouchableOpacity } from "react-native";
|
import { View, Image, Text, TouchableOpacity } from "react-native";
|
||||||
import styles from "@/app/stackStyles/participantStyles";
|
import styles from "@/app/stackStyles/participantStyles";
|
||||||
import Ionicons from "@expo/vector-icons/Ionicons";
|
import Ionicons from "@expo/vector-icons/Ionicons";
|
||||||
import React, {
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
useCallback,
|
import { router, useLocalSearchParams } from "expo-router";
|
||||||
useMemo,
|
import { usePersonContext } from "@/contexts/PersonContext";
|
||||||
useRef,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { router } from "expo-router";
|
|
||||||
import Feather from "@expo/vector-icons/Feather";
|
|
||||||
import BottomSheet, { BottomSheetScrollView } from "@gorhom/bottom-sheet";
|
|
||||||
import {
|
import {
|
||||||
ScrollView,
|
ScrollView,
|
||||||
GestureHandlerRootView,
|
GestureHandlerRootView,
|
||||||
} from "react-native-gesture-handler";
|
} from "react-native-gesture-handler";
|
||||||
import { useShowContext } from "@/contexts/ShowContext";
|
import { useShowContext } from "@/contexts/ShowContext";
|
||||||
import Animated, {
|
import { getPersonHistory } from "@/apis/personApi";
|
||||||
useSharedValue,
|
|
||||||
useAnimatedStyle,
|
|
||||||
withTiming,
|
|
||||||
withRepeat,
|
|
||||||
withSequence,
|
|
||||||
Easing,
|
|
||||||
cancelAnimation,
|
|
||||||
} from "react-native-reanimated";
|
|
||||||
|
|
||||||
export default function ParticipantScreen() {
|
export default function ParticipantScreen() {
|
||||||
|
const { getPersonAppearances, isLoading, getError } = usePersonContext();
|
||||||
|
const [appearances, setAppearances] = useState<
|
||||||
|
{
|
||||||
|
showId: number;
|
||||||
|
seasons: number[];
|
||||||
|
}[]
|
||||||
|
>([]);
|
||||||
const { shows, error, loading } = useShowContext();
|
const { shows, error, loading } = useShowContext();
|
||||||
|
const { participantId, name, season } = useLocalSearchParams();
|
||||||
const bottomSheetRef = useRef<BottomSheet>(null);
|
const numericId = Array.isArray(participantId)
|
||||||
|
? Number(participantId[0])
|
||||||
const [sheetIndex, setSheetIndex] = useState(1);
|
: Number(participantId);
|
||||||
|
|
||||||
const handleSheetChange = useCallback((index: number) => {
|
|
||||||
setSheetIndex(index);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const snapPoints = useMemo(() => ["10%", "10%", "45%"], []);
|
|
||||||
|
|
||||||
const bounce = useSharedValue(0);
|
|
||||||
const expanded = useSharedValue(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sheetIndex === 2) {
|
let active = true;
|
||||||
cancelAnimation(bounce);
|
(async () => {
|
||||||
expanded.value = withTiming(1, { duration: 120 });
|
if (!numericId || Number.isNaN(numericId)) return;
|
||||||
bounce.value = withTiming(-12, { duration: 120 });
|
const data = await getPersonAppearances(numericId);
|
||||||
} else {
|
if (!active) return;
|
||||||
expanded.value = withTiming(0, { duration: 100 });
|
const grouped = data.showIds.map((id) => ({
|
||||||
bounce.value = withRepeat(
|
showId: id,
|
||||||
withSequence(
|
seasons: data.byShow[id],
|
||||||
withTiming(-6, { duration: 250, easing: Easing.out(Easing.quad) }),
|
|
||||||
withTiming(0, { duration: 250, easing: Easing.inOut(Easing.quad) })
|
|
||||||
),
|
|
||||||
-1,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
cancelAnimation(bounce);
|
|
||||||
};
|
|
||||||
}, [sheetIndex, bounce, expanded]);
|
|
||||||
|
|
||||||
const iconAnimatedStyle = useAnimatedStyle(() => ({
|
|
||||||
transform: [
|
|
||||||
{ translateY: bounce.value },
|
|
||||||
{ rotate: `${expanded.value * 180}deg` },
|
|
||||||
],
|
|
||||||
opacity: 1 - expanded.value * 0.3,
|
|
||||||
}));
|
}));
|
||||||
|
setAppearances(grouped);
|
||||||
|
})();
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}, [numericId, getPersonAppearances]);
|
||||||
|
|
||||||
|
const resolved = useMemo(
|
||||||
|
() =>
|
||||||
|
appearances
|
||||||
|
.map((a) => {
|
||||||
|
const show = shows.find((s) => s.id === a.showId);
|
||||||
|
if (!show) return null;
|
||||||
|
return { show, seasons: a.seasons };
|
||||||
|
})
|
||||||
|
.filter(
|
||||||
|
(v): v is { show: (typeof shows)[number]; seasons: number[] } => !!v
|
||||||
|
),
|
||||||
|
[appearances, shows]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GestureHandlerRootView style={styles.mainContainer}>
|
<GestureHandlerRootView style={styles.mainContainer}>
|
||||||
<ScrollView showsVerticalScrollIndicator={false}>
|
<ScrollView showsVerticalScrollIndicator={false}>
|
||||||
<Text style={styles.participantName}>Calvin Ogara</Text>
|
<Text style={styles.participantName}>
|
||||||
|
{name ? (Array.isArray(name) ? name[0] : name) : "Teilnehmer"}
|
||||||
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.closeIcon}
|
style={styles.closeIcon}
|
||||||
onPress={() => router.back()}
|
onPress={() => router.back()}
|
||||||
@@ -97,7 +84,21 @@ export default function ParticipantScreen() {
|
|||||||
|
|
||||||
<View style={styles.performedShowsSection}>
|
<View style={styles.performedShowsSection}>
|
||||||
<Text style={styles.performedShowsTitle}>Auftritte:</Text>
|
<Text style={styles.performedShowsTitle}>Auftritte:</Text>
|
||||||
|
{isLoading(numericId) && (
|
||||||
|
<Text style={{ color: "white", marginTop: 8 }}>Lädt...</Text>
|
||||||
|
)}
|
||||||
|
{getError(numericId) && (
|
||||||
|
<Text style={{ color: "tomato", marginTop: 8 }}>
|
||||||
|
{getError(numericId)}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{!isLoading(numericId) &&
|
||||||
|
resolved.length === 0 &&
|
||||||
|
!getError(numericId) && (
|
||||||
|
<Text style={{ color: "gray", marginTop: 8 }}>
|
||||||
|
Keine Einträge.
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
<ScrollView
|
<ScrollView
|
||||||
horizontal
|
horizontal
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
@@ -106,38 +107,25 @@ export default function ParticipantScreen() {
|
|||||||
marginTop: 15,
|
marginTop: 15,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{shows.map((show, i) => (
|
{resolved.map(({ show, seasons }) => (
|
||||||
<View style={styles.showContainer} key={i}>
|
<TouchableOpacity key={show.id} style={styles.showContainer}>
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: show.thumbnailUri }}
|
source={{ uri: show.thumbnailUri }}
|
||||||
style={styles.showImage}
|
style={styles.showImage}
|
||||||
/>
|
/>
|
||||||
</View>
|
<Text style={styles.showTitle} numberOfLines={2}>
|
||||||
|
{show.title}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.showSeason} numberOfLines={1}>
|
||||||
|
Staffel
|
||||||
|
{seasons.length === 1
|
||||||
|
? ` ${seasons[0]}`
|
||||||
|
: `n ${seasons.join(", ")}`}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
<BottomSheet
|
|
||||||
ref={bottomSheetRef}
|
|
||||||
index={1}
|
|
||||||
snapPoints={snapPoints}
|
|
||||||
enableDynamicSizing={false}
|
|
||||||
onChange={handleSheetChange}
|
|
||||||
backgroundStyle={{ backgroundColor: "hsl(221, 39%, 12%)" }}
|
|
||||||
handleIndicatorStyle={{ backgroundColor: "transparent" }}
|
|
||||||
>
|
|
||||||
<BottomSheetScrollView
|
|
||||||
contentContainerStyle={styles.contentContainer}
|
|
||||||
>
|
|
||||||
<Animated.View
|
|
||||||
style={[
|
|
||||||
{ alignSelf: "center", marginBottom: 20 },
|
|
||||||
iconAnimatedStyle,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Feather name="chevrons-up" size={40} color="white" />
|
|
||||||
</Animated.View>
|
|
||||||
</BottomSheetScrollView>
|
|
||||||
</BottomSheet>
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</GestureHandlerRootView>
|
</GestureHandlerRootView>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -175,7 +175,10 @@ export default function ShowDetails() {
|
|||||||
onPress={() =>
|
onPress={() =>
|
||||||
router.push({
|
router.push({
|
||||||
pathname: "/participant",
|
pathname: "/participant",
|
||||||
params: { participantId: p.id, name: p.name },
|
params: {
|
||||||
|
participantId: p.id,
|
||||||
|
name: p.name,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -89,6 +89,20 @@ const styles = StyleSheet.create({
|
|||||||
margin: 6,
|
margin: 6,
|
||||||
backgroundColor: "#eee",
|
backgroundColor: "#eee",
|
||||||
},
|
},
|
||||||
|
showTitle: {
|
||||||
|
color: "white",
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: "600",
|
||||||
|
textAlign: "center",
|
||||||
|
marginTop: 10,
|
||||||
|
},
|
||||||
|
showSeason: {
|
||||||
|
color: "hsl(0, 0%, 80%)",
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: "400",
|
||||||
|
textAlign: "center",
|
||||||
|
marginTop: 3,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default styles;
|
export default styles;
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const ShowCard = ({
|
|||||||
<View style={styles.streamingServiceIcon}>
|
<View style={styles.streamingServiceIcon}>
|
||||||
<Image
|
<Image
|
||||||
source={{
|
source={{
|
||||||
uri: "https://play-lh.googleusercontent.com/e8u4F0ED6hDMzmjg5cV_C5Sxrzr3xECniwKCD2Q8QfUeVMVRLG41TrsnqroTE7uxk4E",
|
uri: streamingServiceUri,
|
||||||
}}
|
}}
|
||||||
style={[StyleSheet.absoluteFillObject, { borderRadius: 15 }]}
|
style={[StyleSheet.absoluteFillObject, { borderRadius: 15 }]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
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 { getSeason, SeasonParticipant } from "@/apis/seasonApi";
|
||||||
import React, { createContext, useContext, useState, useCallback } from "react";
|
import React, {
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useState,
|
||||||
|
useCallback,
|
||||||
|
ReactNode,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
type SeasonContextType = {
|
type SeasonContextType = {
|
||||||
fetchSeasonParticipants: (
|
fetchSeasonParticipants: (
|
||||||
@@ -11,11 +17,10 @@ type SeasonContextType = {
|
|||||||
|
|
||||||
const SeasonContext = createContext<SeasonContextType | null>(null);
|
const SeasonContext = createContext<SeasonContextType | null>(null);
|
||||||
|
|
||||||
export const SeasonProvider = ({ children }: { children: React.ReactNode }) => {
|
export const SeasonProvider = ({ children }: { children: ReactNode }) => {
|
||||||
const [seasonCache, setSeasonCache] = useState<
|
const [seasonCache, setSeasonCache] = useState<
|
||||||
Record<string, SeasonParticipant[]>
|
Record<string, SeasonParticipant[]>
|
||||||
>({});
|
>({});
|
||||||
|
|
||||||
const [seasonCountCache, setSeasonCountCache] = useState<
|
const [seasonCountCache, setSeasonCountCache] = useState<
|
||||||
Record<number, number>
|
Record<number, number>
|
||||||
>({});
|
>({});
|
||||||
@@ -24,7 +29,6 @@ export const SeasonProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
async (showId: number, seasonNumber: number) => {
|
async (showId: number, seasonNumber: number) => {
|
||||||
const key = `${showId}-${seasonNumber}`;
|
const key = `${showId}-${seasonNumber}`;
|
||||||
if (seasonCache[key]) return seasonCache[key];
|
if (seasonCache[key]) return seasonCache[key];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const season = await getSeason(showId, seasonNumber);
|
const season = await getSeason(showId, seasonNumber);
|
||||||
const participants = season?.participants ?? [];
|
const participants = season?.participants ?? [];
|
||||||
@@ -67,8 +71,8 @@ export const SeasonProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useSeasonContext = () => {
|
export const useSeasonContext = () => {
|
||||||
const context = useContext(SeasonContext);
|
const ctx = useContext(SeasonContext);
|
||||||
if (!context)
|
if (!ctx)
|
||||||
throw new Error("useSeasonContext must be used within a SeasonProvider");
|
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