:api added seasonApi to fetch seasons for a show
This commit is contained in:
77
apis/seasonApi.ts
Normal file
77
apis/seasonApi.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
export type RawSeasonParticipant = {
|
||||||
|
id: { seasonId: number; personId: number };
|
||||||
|
person: {
|
||||||
|
personId: number;
|
||||||
|
name: string;
|
||||||
|
birthDate: string;
|
||||||
|
imageUrl: string | null;
|
||||||
|
};
|
||||||
|
partner: unknown | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RawSeason = {
|
||||||
|
seasonId: number;
|
||||||
|
show: number;
|
||||||
|
seasonNumber: number;
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string | null;
|
||||||
|
moderators: unknown[];
|
||||||
|
seasonParticipants: RawSeasonParticipant[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SeasonParticipant = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
birthYear?: number;
|
||||||
|
imageUri: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Season = {
|
||||||
|
id: number;
|
||||||
|
showId: number;
|
||||||
|
seasonNumber: number;
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string | null;
|
||||||
|
participants: SeasonParticipant[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const SEASON_BASE_URL = "http://45.157.177.99:8080/shows";
|
||||||
|
|
||||||
|
export async function getSeason(
|
||||||
|
showId: number,
|
||||||
|
seasonNumber: number
|
||||||
|
): Promise<Season | null> {
|
||||||
|
// WICHTIG: trailing Slash entfernt
|
||||||
|
const url = `${SEASON_BASE_URL}/${showId}/seasons/${seasonNumber}`;
|
||||||
|
try {
|
||||||
|
console.log("[getSeason] Fetch:", url);
|
||||||
|
const res = await fetch(url);
|
||||||
|
console.log("[getSeason] Status:", res.status);
|
||||||
|
if (res.status === 404) return null;
|
||||||
|
if (!res.ok) throw new Error(`Season fetch failed: ${res.status}`);
|
||||||
|
const raw: RawSeason = await res.json();
|
||||||
|
const participants: SeasonParticipant[] = raw.seasonParticipants.map(
|
||||||
|
(p) => ({
|
||||||
|
id: p.person.personId,
|
||||||
|
name: p.person.name,
|
||||||
|
birthYear: p.person.birthDate
|
||||||
|
? Number(p.person.birthDate.slice(0, 4))
|
||||||
|
: undefined,
|
||||||
|
imageUri:
|
||||||
|
p.person.imageUrl ??
|
||||||
|
"https://via.placeholder.com/300x400.png?text=No+Image",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
id: raw.seasonId,
|
||||||
|
showId: raw.show,
|
||||||
|
seasonNumber: raw.seasonNumber,
|
||||||
|
startDate: raw.startDate,
|
||||||
|
endDate: raw.endDate,
|
||||||
|
participants,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error("getSeason error:", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,11 +26,11 @@ export type Show = {
|
|||||||
running: boolean;
|
running: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const API_URL = "http://45.157.177.99:8080/shows";
|
const SHOW_API_URL = "http://45.157.177.99:8080/shows";
|
||||||
|
|
||||||
export async function getShows(): Promise<Show[]> {
|
export async function getShows(): Promise<Show[]> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(API_URL);
|
const response = await fetch(SHOW_API_URL);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Network response was not ok");
|
throw new Error("Network response was not ok");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export default function HomeScreen() {
|
|||||||
router.push({
|
router.push({
|
||||||
pathname: "/showDetails",
|
pathname: "/showDetails",
|
||||||
params: {
|
params: {
|
||||||
|
id: String(show.id),
|
||||||
title: show.title,
|
title: show.title,
|
||||||
bannerUri: show.bannerUri,
|
bannerUri: show.bannerUri,
|
||||||
description: show.description,
|
description: show.description,
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { ShowProvider } from "@/contexts/ShowContext";
|
import { ShowProvider } from "@/contexts/ShowContext";
|
||||||
|
import { SeasonProvider } from "@/contexts/SeasonContext";
|
||||||
import { Stack } from "expo-router";
|
import { Stack } from "expo-router";
|
||||||
import "react-native-reanimated";
|
import "react-native-reanimated";
|
||||||
|
|
||||||
export default function RootLayout() {
|
export default function RootLayout() {
|
||||||
return (
|
return (
|
||||||
<ShowProvider>
|
<ShowProvider>
|
||||||
|
<SeasonProvider>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
@@ -21,6 +23,7 @@ export default function RootLayout() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
</SeasonProvider>
|
||||||
</ShowProvider>
|
</ShowProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,84 @@
|
|||||||
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, {
|
||||||
|
useCallback,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import { router } from "expo-router";
|
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 Animated, {
|
||||||
|
useSharedValue,
|
||||||
|
useAnimatedStyle,
|
||||||
|
withTiming,
|
||||||
|
withRepeat,
|
||||||
|
withSequence,
|
||||||
|
Easing,
|
||||||
|
cancelAnimation,
|
||||||
|
} from "react-native-reanimated";
|
||||||
|
|
||||||
export default function ParticipantScreen() {
|
export default function ParticipantScreen() {
|
||||||
|
const { shows, error, loading } = useShowContext();
|
||||||
|
|
||||||
|
const bottomSheetRef = useRef<BottomSheet>(null);
|
||||||
|
|
||||||
|
const [sheetIndex, setSheetIndex] = useState(1);
|
||||||
|
|
||||||
|
const handleSheetChange = useCallback((index: number) => {
|
||||||
|
setSheetIndex(index);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const snapPoints = useMemo(() => ["10%", "10%", "45%"], []);
|
||||||
|
|
||||||
|
const bounce = useSharedValue(0);
|
||||||
|
const expanded = useSharedValue(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (sheetIndex === 2) {
|
||||||
|
cancelAnimation(bounce);
|
||||||
|
expanded.value = withTiming(1, { duration: 120 });
|
||||||
|
bounce.value = withTiming(-12, { duration: 120 });
|
||||||
|
} else {
|
||||||
|
expanded.value = withTiming(0, { duration: 100 });
|
||||||
|
bounce.value = withRepeat(
|
||||||
|
withSequence(
|
||||||
|
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,
|
||||||
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GestureHandlerRootView style={styles.mainContainer}>
|
<GestureHandlerRootView style={styles.mainContainer}>
|
||||||
|
<ScrollView showsVerticalScrollIndicator={false}>
|
||||||
<Text style={styles.participantName}>Calvin Ogara</Text>
|
<Text style={styles.participantName}>Calvin Ogara</Text>
|
||||||
<TouchableOpacity style={styles.closeIcon} onPress={() => router.back()}>
|
<TouchableOpacity
|
||||||
|
style={styles.closeIcon}
|
||||||
|
onPress={() => router.back()}
|
||||||
|
>
|
||||||
<Ionicons name="close-circle-outline" size={38} color="white" />
|
<Ionicons name="close-circle-outline" size={38} color="white" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<View style={styles.participantInfoSection}>
|
<View style={styles.participantInfoSection}>
|
||||||
@@ -39,11 +106,39 @@ export default function ParticipantScreen() {
|
|||||||
marginTop: 15,
|
marginTop: 15,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{[...Array(5)].map((show, index) => (
|
{shows.map((show, i) => (
|
||||||
<View style={styles.showContainer} key={index}></View>
|
<View style={styles.showContainer} key={i}>
|
||||||
|
<Image
|
||||||
|
source={{ uri: show.thumbnailUri }}
|
||||||
|
style={styles.showImage}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
))}
|
))}
|
||||||
</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>
|
||||||
</GestureHandlerRootView>
|
</GestureHandlerRootView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useLocalSearchParams, router } from "expo-router";
|
|||||||
import ShowInfo from "@/components/ui/ShowInfo";
|
import ShowInfo from "@/components/ui/ShowInfo";
|
||||||
import ParticipantDetails from "@/components/ParticipantDeatails";
|
import ParticipantDetails from "@/components/ParticipantDeatails";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { useSeasonContext } from "@/contexts/SeasonContext";
|
||||||
import {
|
import {
|
||||||
Dimensions,
|
Dimensions,
|
||||||
Image,
|
Image,
|
||||||
@@ -11,15 +12,58 @@ import {
|
|||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
|
import * as WebBrowser from "expo-web-browser";
|
||||||
import styles from "./stackStyles/showDetailStyles";
|
import styles from "./stackStyles/showDetailStyles";
|
||||||
import { parseQueryParams } from "expo-router/build/fork/getStateFromPath-forks";
|
|
||||||
export default function ShowDetails() {
|
export default function ShowDetails() {
|
||||||
const { bannerUri, description, concept, genres, streamingService } =
|
const { bannerUri, description, concept, genres, streamingService, id } =
|
||||||
useLocalSearchParams();
|
useLocalSearchParams();
|
||||||
const [selectedParticipants, setSelectedParticipants] =
|
const [selectedParticipants, setSelectedParticipants] =
|
||||||
React.useState<boolean>(true);
|
React.useState<boolean>(true);
|
||||||
const [selectedSeason, setSelectedSeason] = React.useState<number>(1);
|
const [selectedSeason, setSelectedSeason] = React.useState<number>(1);
|
||||||
|
const showId = Number(id);
|
||||||
|
const { fetchSeasonParticipants, fetchSeasonCount } = useSeasonContext();
|
||||||
|
const [seasonCount, setSeasonCount] = React.useState<number>(0);
|
||||||
|
const [participants, setParticipants] = React.useState<
|
||||||
|
{ id: number; name: string; imageUri: string }[]
|
||||||
|
>([]);
|
||||||
|
const [pLoading, setPLoading] = React.useState(false);
|
||||||
|
const [pError, setPError] = React.useState<string | null>(null);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!showId) return;
|
||||||
|
let active = true;
|
||||||
|
(async () => {
|
||||||
|
const count = await fetchSeasonCount(showId);
|
||||||
|
if (active) {
|
||||||
|
setSeasonCount(count);
|
||||||
|
if (count > 0 && selectedSeason > count) setSelectedSeason(1);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}, [showId, fetchSeasonCount]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!showId || !selectedSeason) return;
|
||||||
|
let active = true;
|
||||||
|
(async () => {
|
||||||
|
setPError(null);
|
||||||
|
setPLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await fetchSeasonParticipants(showId, selectedSeason);
|
||||||
|
if (active) setParticipants(data);
|
||||||
|
} catch {
|
||||||
|
if (active) setPError("Fehler beim Laden");
|
||||||
|
} finally {
|
||||||
|
if (active) setPLoading(false);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}, [showId, selectedSeason, fetchSeasonParticipants]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.mainContainer}>
|
<View style={styles.mainContainer}>
|
||||||
@@ -37,8 +81,8 @@ export default function ShowDetails() {
|
|||||||
style={styles.showImage}
|
style={styles.showImage}
|
||||||
/>
|
/>
|
||||||
<ShowInfo
|
<ShowInfo
|
||||||
seasons={10}
|
seasons={seasonCount}
|
||||||
participants={150}
|
participants={participants.length}
|
||||||
streamingService={streamingService as string}
|
streamingService={streamingService as string}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -88,23 +132,25 @@ export default function ShowDetails() {
|
|||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
contentContainerStyle={styles.seasonList}
|
contentContainerStyle={styles.seasonList}
|
||||||
>
|
>
|
||||||
{[...Array(10).keys()].map((season) => (
|
{Array.from({ length: seasonCount }, (_, idx) => idx + 1).map(
|
||||||
|
(season) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={season}
|
key={season}
|
||||||
style={[
|
style={[
|
||||||
styles.seasonContainer,
|
styles.seasonContainer,
|
||||||
{
|
{
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
selectedSeason === season + 1
|
selectedSeason === season
|
||||||
? "#199edb"
|
? "#199edb"
|
||||||
: "hsl(0, 0%, 20%)",
|
: "hsl(0, 0%, 20%)",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
onPress={() => setSelectedSeason(season + 1)}
|
onPress={() => setSelectedSeason(season)}
|
||||||
>
|
>
|
||||||
<Text style={styles.seasonLabel}>{season + 1}</Text>
|
<Text style={styles.seasonLabel}>{season}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
)
|
||||||
|
)}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -114,90 +160,38 @@ export default function ShowDetails() {
|
|||||||
styles.participantSection,
|
styles.participantSection,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{[0, 1, 2].map((column) => (
|
{pError && (
|
||||||
|
<Text style={{ color: "tomato", marginBottom: 8 }}>
|
||||||
|
{pError}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{!pLoading && !pError && participants.length === 0 && (
|
||||||
|
<Text style={{ color: "gray" }}>Keine Teilnehmer.</Text>
|
||||||
|
)}
|
||||||
|
{participants.map((p) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={column}
|
key={p.id}
|
||||||
style={styles.participantContainer}
|
style={styles.participantContainer}
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
router.push({
|
router.push({
|
||||||
pathname: "/participant",
|
pathname: "/participant",
|
||||||
|
params: { participantId: p.id, name: p.name },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{column === 0 && (
|
|
||||||
<>
|
|
||||||
<Image
|
<Image
|
||||||
source={{
|
source={{ uri: p.imageUri }}
|
||||||
uri: "https://amp.infranken.de/storage/image/2/2/7/8/4408722_hat-calvin-o-bei-vip-are-you-the-one-eine-favoritin-die-indizien_noscale_1EywMa_HqGfqa.jpg",
|
|
||||||
}}
|
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Text style={styles.participantLabel} numberOfLines={2}>
|
||||||
<Text style={styles.participantLabel}>
|
{p.name}
|
||||||
Calvin Lesra Ogara
|
|
||||||
</Text>
|
</Text>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{column === 1 && (
|
|
||||||
<>
|
|
||||||
<Image
|
|
||||||
source={{
|
|
||||||
uri: "https://content.promiflash.de/article-images/square600/love-island-granate-sandra-2.jpg",
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
borderRadius: 10,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Text style={styles.participantLabel}>Sandra Janina</Text>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{column === 2 && (
|
|
||||||
<>
|
|
||||||
<Image
|
|
||||||
source={{
|
|
||||||
uri: "https://static.wikia.nocookie.net/toohottohandle/images/e/e4/GER_S1_Kevin_Njie.jpg/revision/latest?cb=20240225192711",
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
borderRadius: 10,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Text style={styles.participantLabel}>Kevin Njie</Text>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
{[0, 1, 2].map((column) => (
|
|
||||||
<View
|
|
||||||
key={column}
|
|
||||||
style={[styles.participantContainer, { marginTop: 20 }]}
|
|
||||||
>
|
|
||||||
{column === 0 && (
|
|
||||||
<>
|
|
||||||
<Image
|
|
||||||
source={{
|
|
||||||
uri: "https://content.promiflash.de/article-images/square600/sidar-are-you-the-one-kandidat-2023.jpg",
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
borderRadius: 10,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Text style={styles.participantLabel}>Single Sidar</Text>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -49,11 +49,10 @@ const styles = StyleSheet.create({
|
|||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
},
|
},
|
||||||
performedShowsSection: {
|
performedShowsSection: {
|
||||||
marginTop: 0,
|
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: 375,
|
||||||
paddingHorizontal: 20,
|
paddingLeft: 15,
|
||||||
paddingVertical: 10,
|
paddingBottom: 20,
|
||||||
backgroundColor: "hsl(221, 39%, 0%)",
|
backgroundColor: "hsl(221, 39%, 0%)",
|
||||||
},
|
},
|
||||||
performedShowsTitle: {
|
performedShowsTitle: {
|
||||||
@@ -69,6 +68,27 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
marginRight: 15,
|
marginRight: 15,
|
||||||
},
|
},
|
||||||
|
showImage: {
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
borderRadius: 10,
|
||||||
|
},
|
||||||
|
showLabel: {
|
||||||
|
color: "white",
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: "600",
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
contentContainer: {
|
||||||
|
flex: 1,
|
||||||
|
padding: 10,
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
itemContainer: {
|
||||||
|
padding: 6,
|
||||||
|
margin: 6,
|
||||||
|
backgroundColor: "#eee",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default styles;
|
export default styles;
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ const styles = StyleSheet.create({
|
|||||||
width: 110,
|
width: 110,
|
||||||
backgroundColor: "hsl(336, 79%, 63%)",
|
backgroundColor: "hsl(336, 79%, 63%)",
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
|
marginTop: 30,
|
||||||
},
|
},
|
||||||
participantSection: {
|
participantSection: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
|
|||||||
74
contexts/SeasonContext.tsx
Normal file
74
contexts/SeasonContext.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { getSeason, SeasonParticipant } from "@/apis/seasonApi";
|
||||||
|
import React, { createContext, useContext, useState, useCallback } from "react";
|
||||||
|
|
||||||
|
type SeasonContextType = {
|
||||||
|
fetchSeasonParticipants: (
|
||||||
|
showId: number,
|
||||||
|
seasonNumber: number
|
||||||
|
) => Promise<SeasonParticipant[]>;
|
||||||
|
fetchSeasonCount: (showId: number) => Promise<number>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SeasonContext = createContext<SeasonContextType | null>(null);
|
||||||
|
|
||||||
|
export const SeasonProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
const [seasonCache, setSeasonCache] = useState<
|
||||||
|
Record<string, SeasonParticipant[]>
|
||||||
|
>({});
|
||||||
|
|
||||||
|
const [seasonCountCache, setSeasonCountCache] = useState<
|
||||||
|
Record<number, number>
|
||||||
|
>({});
|
||||||
|
|
||||||
|
const fetchSeasonParticipants = useCallback(
|
||||||
|
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 ?? [];
|
||||||
|
setSeasonCache((c) => ({ ...c, [key]: participants }));
|
||||||
|
return participants;
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[seasonCache]
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchSeasonCount = useCallback(
|
||||||
|
async (showId: number) => {
|
||||||
|
if (seasonCountCache[showId] !== undefined)
|
||||||
|
return seasonCountCache[showId];
|
||||||
|
let n = 0;
|
||||||
|
for (let s = 1; s <= 50; s++) {
|
||||||
|
try {
|
||||||
|
const season = await getSeason(showId, s);
|
||||||
|
if (!season) break;
|
||||||
|
n = s;
|
||||||
|
} catch {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setSeasonCountCache((c) => ({ ...c, [showId]: n }));
|
||||||
|
return n;
|
||||||
|
},
|
||||||
|
[seasonCountCache]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SeasonContext.Provider
|
||||||
|
value={{ fetchSeasonParticipants, fetchSeasonCount }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SeasonContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSeasonContext = () => {
|
||||||
|
const context = useContext(SeasonContext);
|
||||||
|
if (!context)
|
||||||
|
throw new Error("useSeasonContext must be used within a SeasonProvider");
|
||||||
|
return context;
|
||||||
|
};
|
||||||
48
package-lock.json
generated
48
package-lock.json
generated
@@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/metro-runtime": "~6.1.2",
|
"@expo/metro-runtime": "~6.1.2",
|
||||||
"@expo/vector-icons": "^15.0.2",
|
"@expo/vector-icons": "^15.0.2",
|
||||||
|
"@gorhom/bottom-sheet": "^5",
|
||||||
"@react-navigation/bottom-tabs": "^7.3.10",
|
"@react-navigation/bottom-tabs": "^7.3.10",
|
||||||
"@react-navigation/elements": "^2.3.8",
|
"@react-navigation/elements": "^2.3.8",
|
||||||
"@react-navigation/native": "^7.1.6",
|
"@react-navigation/native": "^7.1.6",
|
||||||
@@ -18,7 +19,7 @@
|
|||||||
"expo-constants": "~18.0.9",
|
"expo-constants": "~18.0.9",
|
||||||
"expo-font": "~14.0.8",
|
"expo-font": "~14.0.8",
|
||||||
"expo-haptics": "~15.0.7",
|
"expo-haptics": "~15.0.7",
|
||||||
"expo-image": "~3.0.8",
|
"expo-image": "~3.0.9",
|
||||||
"expo-linking": "~8.0.8",
|
"expo-linking": "~8.0.8",
|
||||||
"expo-router": "~6.0.10",
|
"expo-router": "~6.0.10",
|
||||||
"expo-splash-screen": "~31.0.10",
|
"expo-splash-screen": "~31.0.10",
|
||||||
@@ -2265,6 +2266,45 @@
|
|||||||
"@babel/highlight": "^7.10.4"
|
"@babel/highlight": "^7.10.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@gorhom/bottom-sheet": {
|
||||||
|
"version": "5.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@gorhom/bottom-sheet/-/bottom-sheet-5.2.6.tgz",
|
||||||
|
"integrity": "sha512-vmruJxdiUGDg+ZYcDmS30XDhq/h/+QkINOI5LY/uGjx8cPGwgJW0H6AB902gNTKtccbiKe/rr94EwdmIEz+LAQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@gorhom/portal": "1.0.14",
|
||||||
|
"invariant": "^2.2.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-native": "*",
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*",
|
||||||
|
"react-native-gesture-handler": ">=2.16.1",
|
||||||
|
"react-native-reanimated": ">=3.16.0 || >=4.0.0-"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-native": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@gorhom/portal": {
|
||||||
|
"version": "1.0.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/@gorhom/portal/-/portal-1.0.14.tgz",
|
||||||
|
"integrity": "sha512-MXyL4xvCjmgaORr/rtryDNFy3kU4qUbKlwtQqqsygd0xX3mhKjOLn6mQK8wfu0RkoE0pBE0nAasRoHua+/QZ7A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"nanoid": "^3.3.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanfs/core": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||||
@@ -6699,9 +6739,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo-image": {
|
"node_modules/expo-image": {
|
||||||
"version": "3.0.8",
|
"version": "3.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/expo-image/-/expo-image-3.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/expo-image/-/expo-image-3.0.9.tgz",
|
||||||
"integrity": "sha512-L83fTHVjvE5hACxUXPk3dpABteI/IypeqxKMeOAAcT2eB/jbqT53ddsYKEvKAP86eoByQ7+TCtw9AOUizEtaTQ==",
|
"integrity": "sha512-GkPIjeqrODMBdpbRWOzbwiq8ztxjgq1rdZrnqwt/pzQavgXPlr4rW/7aigue9Jm5t5vebhMNAuc1A/XIXXqpcA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"expo": "*",
|
"expo": "*",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/metro-runtime": "~6.1.2",
|
"@expo/metro-runtime": "~6.1.2",
|
||||||
"@expo/vector-icons": "^15.0.2",
|
"@expo/vector-icons": "^15.0.2",
|
||||||
|
"@gorhom/bottom-sheet": "^5",
|
||||||
"@react-navigation/bottom-tabs": "^7.3.10",
|
"@react-navigation/bottom-tabs": "^7.3.10",
|
||||||
"@react-navigation/elements": "^2.3.8",
|
"@react-navigation/elements": "^2.3.8",
|
||||||
"@react-navigation/native": "^7.1.6",
|
"@react-navigation/native": "^7.1.6",
|
||||||
@@ -21,7 +22,7 @@
|
|||||||
"expo-constants": "~18.0.9",
|
"expo-constants": "~18.0.9",
|
||||||
"expo-font": "~14.0.8",
|
"expo-font": "~14.0.8",
|
||||||
"expo-haptics": "~15.0.7",
|
"expo-haptics": "~15.0.7",
|
||||||
"expo-image": "~3.0.8",
|
"expo-image": "~3.0.9",
|
||||||
"expo-linking": "~8.0.8",
|
"expo-linking": "~8.0.8",
|
||||||
"expo-router": "~6.0.10",
|
"expo-router": "~6.0.10",
|
||||||
"expo-splash-screen": "~31.0.10",
|
"expo-splash-screen": "~31.0.10",
|
||||||
|
|||||||
Reference in New Issue
Block a user