From 98a2067b8d9068a94f6e35b9c380d49a2a45af5b Mon Sep 17 00:00:00 2001 From: Yordan Simeonov <118773725+Cron1cle@users.noreply.github.com> Date: Mon, 20 Oct 2025 18:45:01 +0200 Subject: [PATCH] api: reconfigered api handling --- apis/autoCompleteApi.ts | 3 +- apis/personApi.ts | 60 --------------- apis/searchApi.ts | 3 +- apis/seasonApi.ts | 56 ++++++++------ app/_layout.tsx | 8 +- app/participant.tsx | 63 ++++------------ app/showDetails.tsx | 38 ++++++---- contexts/PersonContext.tsx | 145 ------------------------------------- contexts/SeasonContext.tsx | 31 +++++++- 9 files changed, 112 insertions(+), 295 deletions(-) delete mode 100644 apis/personApi.ts delete mode 100644 contexts/PersonContext.tsx diff --git a/apis/autoCompleteApi.ts b/apis/autoCompleteApi.ts index e9fb591..418ccdf 100644 --- a/apis/autoCompleteApi.ts +++ b/apis/autoCompleteApi.ts @@ -16,7 +16,8 @@ export async function getAutoComplete( const url = `${DISCOVER_BASE}/autoComplete?q=${encodeURIComponent( query )}&limit=${limit}`; - const res = await fetch(url, { signal }); + const apiKey = process.env.EXPO_PUBLIC_API_KEY; + const res = await fetch(url, { signal, headers: { 'Content-Type': 'application/json', "X-API-Key": apiKey ?? "", } }); if (!res.ok) throw new Error("AutoComplete failed " + res.status); const data: unknown = await res.json(); if (!Array.isArray(data)) return []; diff --git a/apis/personApi.ts b/apis/personApi.ts deleted file mode 100644 index 3f5f2ff..0000000 --- a/apis/personApi.ts +++ /dev/null @@ -1,60 +0,0 @@ -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; - partner?: { - id: number; - name: string; - birthDate?: string; - imageUri: string | null; - } | null; -}; - -const PERSON_API_BASE = "http://45.157.177.99:8080/persons"; - -export async function getPersonHistory( - personId: number -): Promise { - 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((season) => { - const me = season.seasonParticipants.find( - (p) => p.person.personId === personId - ); - let partner: PersonHistoryEntry["partner"] = null; - if (me?.partner?.person) { - partner = { - id: me.partner.person.personId, - name: me.partner.person.name, - imageUri: me.partner.person.imageUrl ?? null, - }; - } - return { - showId: season.show, - seasonId: season.seasonId, - seasonNumber: season.seasonNumber, - partner, - }; - }); - } catch (e) { - console.error("getPersonHistory error:", e); - return []; - } -} diff --git a/apis/searchApi.ts b/apis/searchApi.ts index 6602dbc..cec8484 100644 --- a/apis/searchApi.ts +++ b/apis/searchApi.ts @@ -20,7 +20,8 @@ export async function getSearchResults( filteredTags.join(",") )}&limit=${limit}`; - const res = await fetch(url, { signal }); + const apiKey = process.env.EXPO_PUBLIC_API_KEY; + const res = await fetch(url, { signal, headers: { 'Content-Type': 'application/json', "X-API-Key": apiKey ?? "", } }); if (!res.ok) throw new Error("AutoComplete failed " + res.status); const data: unknown = await res.json(); diff --git a/apis/seasonApi.ts b/apis/seasonApi.ts index 262271d..92fa065 100644 --- a/apis/seasonApi.ts +++ b/apis/seasonApi.ts @@ -1,22 +1,25 @@ + export type RawSeasonParticipant = { - id: { seasonId: number; personId: number }; - person: { + personId: number; + name: string; + birthDate?: string; + imageUrl?: string | null; + partner?: { personId: number; name: string; - birthDate: string; - imageUrl: string | null; + 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[]; + show?: number; + moderators?: unknown[]; }; export type SeasonParticipant = { @@ -43,27 +46,37 @@ export async function getSeason( ): Promise { const url = `${SEASON_BASE_URL}/${showId}/seasons/${seasonNumber}`; try { + const apiKey = process.env.EXPO_PUBLIC_API_KEY; console.log("[getSeason] Fetch:", url); - const res = await fetch(url); + const res = await fetch(url, { + headers: { + Accept: "application/json", + "Content-Type": "application/json", + "X-API-Key": apiKey ?? "", + }, + }); 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://i.pravatar.cc/300?img=${Math.random() * 70}`, - }) - ); + + const participants: SeasonParticipant[] = Array.isArray( + raw.seasonParticipants + ) + ? raw.seasonParticipants.map((p) => ({ + id: p.personId, + name: p.name, + birthYear: p.birthDate ? Number(p.birthDate.slice(0, 4)) : undefined, + imageUri: + p.imageUrl ?? + `https://i.pravatar.cc/300?img=${Math.floor(Math.random() * 70) + 1}`, + })) + : []; + return { id: raw.seasonId, - showId: raw.show, + showId, seasonNumber: raw.seasonNumber, startDate: raw.startDate, endDate: raw.endDate, @@ -74,3 +87,4 @@ export async function getSeason( throw e; } } + diff --git a/app/_layout.tsx b/app/_layout.tsx index b0138a9..668beb3 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,7 +1,7 @@ -import { ShowProvider } from "@/contexts/ShowContext"; import { SeasonProvider } from "@/contexts/SeasonContext"; +import { ShowProvider } from "@/contexts/ShowContext"; import { StreamingServiceProvider } from "@/contexts/StreamingServiceContext"; -import { PersonProvider } from "@/contexts/PersonContext"; + import { DiscoveryProvider } from "@/contexts/DiscoveryContext"; import { Stack } from "expo-router"; import "react-native-reanimated"; @@ -11,7 +11,7 @@ export default function RootLayout() { - + @@ -30,7 +30,7 @@ export default function RootLayout() { /> - + diff --git a/app/participant.tsx b/app/participant.tsx index 56c5fb0..72099ae 100644 --- a/app/participant.tsx +++ b/app/participant.tsx @@ -1,47 +1,28 @@ -import { View, Image, Text, TouchableOpacity } from "react-native"; import styles from "@/app/stackStyles/participantStyles"; import Ionicons from "@expo/vector-icons/Ionicons"; -import React, { useEffect, useMemo, useState } from "react"; import { router, useLocalSearchParams } from "expo-router"; -import { usePersonContext } from "@/contexts/PersonContext"; -import * as WebBrowser from "expo-web-browser"; -import { - ScrollView, - GestureHandlerRootView, -} from "react-native-gesture-handler"; +import React, { useMemo, useState } from "react"; +import { Image, Text, TouchableOpacity, View } from "react-native"; + import { useShowContext } from "@/contexts/ShowContext"; +import { + GestureHandlerRootView, + ScrollView, +} from "react-native-gesture-handler"; export default function ParticipantScreen() { - const { getPersonAppearances, isLoading, getError } = usePersonContext(); - const [appearances, setAppearances] = useState< + + const [appearances,] = useState< { showId: number; seasons: number[]; }[] >([]); - const { shows, error, loading } = useShowContext(); - const { participantId, name, season } = useLocalSearchParams(); - const numericId = Array.isArray(participantId) - ? Number(participantId[0]) - : Number(participantId); + const { shows} = useShowContext(); + const { name } = useLocalSearchParams(); + - useEffect(() => { - 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], - partners: data.partnersByShow[id] || [], - })); - setAppearances(grouped as any); - })(); - return () => { - active = false; - }; - }, [numericId, getPersonAppearances]); + const resolved = useMemo( () => @@ -88,21 +69,9 @@ export default function ParticipantScreen() { Auftritte: - {isLoading(numericId) && ( - Lädt... - )} - {getError(numericId) && ( - - {getError(numericId)} - - )} - {!isLoading(numericId) && - resolved.length === 0 && - !getError(numericId) && ( - - Keine Einträge. - - )} + + + (true); const [selectedSeason, setSelectedSeason] = React.useState(1); const showId = Number(id); - const { fetchSeasonParticipants, fetchSeasonCount } = useSeasonContext(); + const { fetchSeasonParticipants, fetchSeasonCount, fetchSeasonDates } = useSeasonContext(); const [seasonCount, setSeasonCount] = React.useState(0); const [participants, setParticipants] = React.useState< { id: number; name: string; imageUri: string }[] @@ -62,15 +61,21 @@ export default function ShowDetails() { }; }, [showId, fetchSeasonCount, selectedSeason]); - React.useEffect(() => { + 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); + const [data, dates] = await Promise.all([ + fetchSeasonParticipants(showId, selectedSeason), + fetchSeasonDates(showId, selectedSeason), + ]); + if (active) { + setParticipants(data); + setStartDate(dates?.startDate); + } } catch { if (active) setPError("Fehler beim Laden"); } finally { @@ -80,14 +85,18 @@ export default function ShowDetails() { return () => { active = false; }; - }, [showId, selectedSeason, fetchSeasonParticipants]); + }, [showId, selectedSeason, fetchSeasonParticipants, fetchSeasonDates]); - const startDateObj = new Date(startDate as string); - const formattedStartDate = startDateObj.toLocaleDateString("de-DE", { - day: "2-digit", - month: "long", - year: "numeric", - }); + const formattedStartDate = React.useMemo(() => { + if (!startDate) return ""; + const d = new Date(startDate); + if (isNaN(d.getTime())) return ""; + return d.toLocaleDateString("de-DE", { + day: "2-digit", + month: "long", + year: "numeric", + }); + }, [startDate]); return ( @@ -98,7 +107,10 @@ export default function ShowDetails() { paddingBottom: Dimensions.get("window").height * 0.1, }} > - {formattedStartDate} + + {formattedStartDate ? ( + {formattedStartDate} + ) : null} ; - showIds: number[]; - partnersByShow: Record< - number, - { seasonNumber: number; partner?: PersonHistoryEntry["partner"] }[] - >; -}; - -type PersonContextType = { - getPersonAppearances: (personId: number) => Promise; - getShowIds: (personId: number) => Promise; - getSeasonsForShow: (personId: number, showId: number) => Promise; - isLoading: (personId: number) => boolean; - getError: (personId: number) => string | null; - invalidatePerson: (personId: number) => void; -}; - -const PersonContext = createContext(null); - -export const PersonProvider = ({ children }: { children: ReactNode }) => { - const [cache, setCache] = useState>({}); - const [loading, setLoading] = useState>({}); - const [errors, setErrors] = useState>({}); - - const buildAppearances = ( - entries: PersonHistoryEntry[] - ): PersonAppearances => { - const byShowSet: Record> = {}; - const partnersByShow: PersonAppearances["partnersByShow"] = {}; - for (const e of entries) { - if (!byShowSet[e.showId]) byShowSet[e.showId] = new Set(); - byShowSet[e.showId].add(e.seasonNumber); - if (!partnersByShow[e.showId]) partnersByShow[e.showId] = []; - partnersByShow[e.showId].push({ - seasonNumber: e.seasonNumber, - partner: e.partner ?? undefined, - }); - } - const byShow: Record = Object.fromEntries( - Object.entries(byShowSet).map(([showId, seasons]) => [ - Number(showId), - Array.from(seasons).sort((a, b) => a - b), - ]) - ); - - Object.values(partnersByShow).forEach((arr) => - arr.sort((a, b) => a.seasonNumber - b.seasonNumber) - ); - return { - raw: entries, - byShow, - showIds: Object.keys(byShow) - .map(Number) - .sort((a, b) => a - b), - partnersByShow, - }; - }; - - 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: [], partnersByShow: {} }; - } 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 as Record)[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 ( - - {children} - - ); -}; - -export const usePersonContext = () => { - const ctx = useContext(PersonContext); - if (!ctx) - throw new Error("usePersonContext must be used within PersonProvider"); - return ctx; -}; diff --git a/contexts/SeasonContext.tsx b/contexts/SeasonContext.tsx index b5e9394..e5988ec 100644 --- a/contexts/SeasonContext.tsx +++ b/contexts/SeasonContext.tsx @@ -1,7 +1,6 @@ import { getSeason, SeasonParticipant } from "@/apis/seasonApi"; import React, { createContext, - ReactNode, useCallback, useContext, useState, @@ -13,17 +12,24 @@ type SeasonContextType = { seasonNumber: number ) => Promise; fetchSeasonCount: (showId: number) => Promise; + fetchSeasonDates: ( + showId: number, + seasonNumber: number + ) => Promise<{ startDate?: string; endDate?: string | null } | null>; }; const SeasonContext = createContext(null); -export const SeasonProvider = ({ children }: { children: ReactNode }) => { +export const SeasonProvider = ({ children }: { children: React.ReactNode }) => { const [seasonCache, setSeasonCache] = useState< Record >({}); const [seasonCountCache, setSeasonCountCache] = useState< Record >({}); + const [datesCache, setDatesCache] = useState< + Record + >({}); const fetchSeasonParticipants = useCallback( async (showId: number, seasonNumber: number) => { @@ -61,15 +67,34 @@ export const SeasonProvider = ({ children }: { children: ReactNode }) => { [seasonCountCache] ); + const fetchSeasonDates = useCallback( + async (showId: number, seasonNumber: number) => { + const key = `${showId}-${seasonNumber}`; + if (datesCache[key]) return datesCache[key]; + try { + const season = await getSeason(showId, seasonNumber); + const dates = season + ? { startDate: season.startDate, endDate: season.endDate } + : null; + if (dates) setDatesCache((c) => ({ ...c, [key]: dates })); + return dates; + } catch { + return null; + } + }, + [datesCache] + ); + return ( {children} ); }; + export const useSeasonContext = () => { const ctx = useContext(SeasonContext); if (!ctx)