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

View File

@@ -1,6 +1,7 @@
import styles from "@/app/tabStyles/indexStyles";
import ShowCard from "@/components/ui/ShowCard";
import { useShowContext } from "@/contexts/ShowContext";
import { useStreamingServiceContext } from "@/contexts/StreamingServiceContext";
import { router } from "expo-router";
import React from "react";
import { ActivityIndicator, Text, View } from "react-native";
@@ -11,6 +12,7 @@ import {
export default function HomeScreen() {
const { shows, error, loading } = useShowContext();
const { streamingServices } = useStreamingServiceContext();
if (loading) {
return (
@@ -47,6 +49,11 @@ export default function HomeScreen() {
<ScrollView contentContainerStyle={{ paddingBottom: 30 }}>
{shows.map((show) => {
const showLiveBadge = show.running;
const streamingService =
streamingServices[
`assets.images.streamingServices.${show.streamingService.toLowerCase()}`
];
return (
<ShowCard
key={show.id}
@@ -66,7 +73,7 @@ export default function HomeScreen() {
})
}
imageUri={show.bannerUri}
streamingServiceUri={show.streamingService}
streamingServiceUri={streamingService}
genres={show.genres}
{...(showLiveBadge
? {

View File

@@ -1,5 +1,7 @@
import { ShowProvider } from "@/contexts/ShowContext";
import { SeasonProvider } from "@/contexts/SeasonContext";
import { StreamingServiceProvider } from "@/contexts/StreamingServiceContext";
import { PersonProvider } from "@/contexts/PersonContext";
import { Stack } from "expo-router";
import "react-native-reanimated";
@@ -7,22 +9,26 @@ export default function RootLayout() {
return (
<ShowProvider>
<SeasonProvider>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen
name="showDetails"
options={{
headerShown: false,
}}
/>
<Stack.Screen
name="participant"
options={{
presentation: "fullScreenModal",
headerShown: false,
}}
/>
</Stack>
<StreamingServiceProvider>
<PersonProvider>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen
name="showDetails"
options={{
headerShown: false,
}}
/>
<Stack.Screen
name="participant"
options={{
presentation: "fullScreenModal",
headerShown: false,
}}
/>
</Stack>
</PersonProvider>
</StreamingServiceProvider>
</SeasonProvider>
</ShowProvider>
);

View File

@@ -1,80 +1,67 @@
import { View, Image, Text, TouchableOpacity } from "react-native";
import styles from "@/app/stackStyles/participantStyles";
import Ionicons from "@expo/vector-icons/Ionicons";
import React, {
useCallback,
useMemo,
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 React, { useEffect, useMemo, useState } from "react";
import { router, useLocalSearchParams } from "expo-router";
import { usePersonContext } from "@/contexts/PersonContext";
import {
ScrollView,
GestureHandlerRootView,
} from "react-native-gesture-handler";
import { useShowContext } from "@/contexts/ShowContext";
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
withRepeat,
withSequence,
Easing,
cancelAnimation,
} from "react-native-reanimated";
import { getPersonHistory } from "@/apis/personApi";
export default function ParticipantScreen() {
const { getPersonAppearances, isLoading, getError } = usePersonContext();
const [appearances, setAppearances] = useState<
{
showId: number;
seasons: number[];
}[]
>([]);
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);
const { participantId, name, season } = useLocalSearchParams();
const numericId = Array.isArray(participantId)
? Number(participantId[0])
: Number(participantId);
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
);
}
let active = true;
(async () => {
if (!numericId || Number.isNaN(numericId)) return;
const data = await getPersonAppearances(numericId);
if (!active) return;
const grouped = data.showIds.map((id) => ({
showId: id,
seasons: data.byShow[id],
}));
setAppearances(grouped);
})();
return () => {
cancelAnimation(bounce);
active = false;
};
}, [sheetIndex, bounce, expanded]);
}, [numericId, getPersonAppearances]);
const iconAnimatedStyle = useAnimatedStyle(() => ({
transform: [
{ translateY: bounce.value },
{ rotate: `${expanded.value * 180}deg` },
],
opacity: 1 - expanded.value * 0.3,
}));
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 (
<GestureHandlerRootView style={styles.mainContainer}>
<ScrollView showsVerticalScrollIndicator={false}>
<Text style={styles.participantName}>Calvin Ogara</Text>
<Text style={styles.participantName}>
{name ? (Array.isArray(name) ? name[0] : name) : "Teilnehmer"}
</Text>
<TouchableOpacity
style={styles.closeIcon}
onPress={() => router.back()}
@@ -97,7 +84,21 @@ export default function ParticipantScreen() {
<View style={styles.performedShowsSection}>
<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
horizontal
showsHorizontalScrollIndicator={false}
@@ -106,38 +107,25 @@ export default function ParticipantScreen() {
marginTop: 15,
}}
>
{shows.map((show, i) => (
<View style={styles.showContainer} key={i}>
{resolved.map(({ show, seasons }) => (
<TouchableOpacity key={show.id} style={styles.showContainer}>
<Image
source={{ uri: show.thumbnailUri }}
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>
</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>
);

View File

@@ -175,7 +175,10 @@ export default function ShowDetails() {
onPress={() =>
router.push({
pathname: "/participant",
params: { participantId: p.id, name: p.name },
params: {
participantId: p.id,
name: p.name,
},
})
}
>

View File

@@ -89,6 +89,20 @@ const styles = StyleSheet.create({
margin: 6,
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;