api: reconfigered api handling

This commit is contained in:
Yordan Simeonov
2025-10-20 18:45:01 +02:00
parent d0194f0dc2
commit 98a2067b8d
9 changed files with 112 additions and 295 deletions

View File

@@ -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 [];

View File

@@ -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<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((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 [];
}
}

View File

@@ -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();

View File

@@ -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<Season | null> {
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;
}
}

View File

@@ -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() {
<ShowProvider>
<SeasonProvider>
<StreamingServiceProvider>
<PersonProvider>
<DiscoveryProvider>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
@@ -30,7 +30,7 @@ export default function RootLayout() {
/>
</Stack>
</DiscoveryProvider>
</PersonProvider>
</StreamingServiceProvider>
</SeasonProvider>
</ShowProvider>

View File

@@ -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() {
<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: "red", 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}

View File

@@ -23,14 +23,13 @@ export default function ShowDetails() {
genres,
streamingService,
id,
endDate,
} = useLocalSearchParams();
const [selectedParticipants, setSelectedParticipants] =
React.useState<boolean>(true);
const [selectedSeason, setSelectedSeason] = React.useState<number>(1);
const showId = Number(id);
const { fetchSeasonParticipants, fetchSeasonCount } = useSeasonContext();
const { fetchSeasonParticipants, fetchSeasonCount, fetchSeasonDates } = useSeasonContext();
const [seasonCount, setSeasonCount] = React.useState<number>(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 (
<View style={styles.mainContainer}>
@@ -98,7 +107,10 @@ export default function ShowDetails() {
paddingBottom: Dimensions.get("window").height * 0.1,
}}
>
<Text style={styles.startDate}>{formattedStartDate}</Text>
{formattedStartDate ? (
<Text style={styles.startDate}>{formattedStartDate}</Text>
) : null}
<ShowInfo
seasons={seasonCount}
participants={participants.length}

View File

@@ -1,145 +0,0 @@
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[];
partnersByShow: Record<
number,
{ seasonNumber: number; partner?: PersonHistoryEntry["partner"] }[]
>;
};
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 byShowSet: Record<number, Set<number>> = {};
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<number, number[]> = 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<number, number[]>)[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;
};

View File

@@ -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<SeasonParticipant[]>;
fetchSeasonCount: (showId: number) => Promise<number>;
fetchSeasonDates: (
showId: number,
seasonNumber: number
) => Promise<{ startDate?: string; endDate?: string | null } | null>;
};
const SeasonContext = createContext<SeasonContextType | null>(null);
export const SeasonProvider = ({ children }: { children: ReactNode }) => {
export const SeasonProvider = ({ children }: { children: React.ReactNode }) => {
const [seasonCache, setSeasonCache] = useState<
Record<string, SeasonParticipant[]>
>({});
const [seasonCountCache, setSeasonCountCache] = useState<
Record<number, number>
>({});
const [datesCache, setDatesCache] = useState<
Record<string, { startDate?: string; endDate?: string | null }>
>({});
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 (
<SeasonContext.Provider
value={{ fetchSeasonParticipants, fetchSeasonCount }}
value={{ fetchSeasonParticipants, fetchSeasonCount, fetchSeasonDates }}
>
{children}
</SeasonContext.Provider>
);
};
export const useSeasonContext = () => {
const ctx = useContext(SeasonContext);
if (!ctx)