@@ -1,5 +1,5 @@
|
||||
import { AutoCompleteItem } from "@/apis/autoCompleteApi";
|
||||
import { getSearchResults, SearchResultItem } from "@/apis/searchApi";
|
||||
import { SearchResultItem } from "@/apis/searchApi";
|
||||
import { Season } from "@/apis/seasonApi";
|
||||
import { Show } from "@/apis/showApi";
|
||||
import styles from "@/app/tabStyles/indexStyles";
|
||||
@@ -10,6 +10,7 @@ import Feather from "@expo/vector-icons/Feather";
|
||||
import React from "react";
|
||||
import { Keyboard, ScrollView, Text, TextInput, TouchableOpacity, View } from "react-native";
|
||||
|
||||
import { useSearch } from "@/hooks/useSearch";
|
||||
import { getShowById } from "@/apis/showApi";
|
||||
import PersonRow from "@/components/discovery/PersonRow";
|
||||
import SeasonCarousel from "@/components/discovery/SeasonCarousel";
|
||||
@@ -20,25 +21,19 @@ export default function ExploreScreen() {
|
||||
const { query, setQuery, suggestions } = useDiscoveryContext();
|
||||
|
||||
const [tags, setTags] = React.useState<AutoCompleteItem[]>([]);
|
||||
const [results, setResults] = React.useState<SearchResultItem[]>([]);
|
||||
const tagStrings = React.useMemo(() => tags.map((t) => t.text), [tags]);
|
||||
const { data: results = [] } = useSearch(tagStrings);
|
||||
|
||||
// Show metadata cache by id (filled from SHOW results and lazy-loaded by id)
|
||||
const [showsById, setShowsById] = React.useState<Record<number, Show>>({});
|
||||
|
||||
// --- helpers ---
|
||||
const tagStrings = React.useMemo(() => tags.map((t) => t.text), [tags]);
|
||||
|
||||
|
||||
function tagAdded(tag: AutoCompleteItem) {
|
||||
const nextTags = tags.some((t) => t.text === tag.text) ? tags : [...tags, tag];
|
||||
setTags(nextTags);
|
||||
|
||||
const inputs = nextTags.map((t) => t.text);
|
||||
getSearchResults(inputs, 50)
|
||||
.then((items) => {
|
||||
setResults(items || []);
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
setQuery("");
|
||||
Keyboard.dismiss();
|
||||
}
|
||||
@@ -46,12 +41,6 @@ export default function ExploreScreen() {
|
||||
function tagRemoved(tag: AutoCompleteItem) {
|
||||
const nextTags = tags.filter((t) => t.text !== tag.text);
|
||||
setTags(nextTags);
|
||||
const inputs = nextTags.map((t) => t.text);
|
||||
getSearchResults(inputs, 50)
|
||||
.then((items) => {
|
||||
setResults(items || []);
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
// Keep our local show cache in sync with SHOW items returned by search
|
||||
@@ -135,7 +124,7 @@ export default function ExploreScreen() {
|
||||
<Text style={[styles.title, { fontSize: 28 }]}>Durchsuchen</Text>
|
||||
</View>
|
||||
|
||||
<View style={{ paddingHorizontal: 10 }}>
|
||||
<View style={styles.sectionContainer}>
|
||||
{/* Search bar */}
|
||||
<View style={styles.searchContainer}>
|
||||
<TextInput
|
||||
@@ -143,13 +132,7 @@ export default function ExploreScreen() {
|
||||
onChangeText={setQuery}
|
||||
placeholder="Wonach suchst du?"
|
||||
placeholderTextColor=""
|
||||
style={{
|
||||
fontSize: 18,
|
||||
fontWeight: "500",
|
||||
color: "hsl(221, 39%, 80%)",
|
||||
width: "90%",
|
||||
height: "100%",
|
||||
}}
|
||||
style={styles.searchInput}
|
||||
returnKeyType="search"
|
||||
onSubmitEditing={() => {
|
||||
if (!query.trim()) return;
|
||||
@@ -205,8 +188,8 @@ export default function ExploreScreen() {
|
||||
<ScrollView keyboardShouldPersistTaps="handled">
|
||||
{/* Personen Section (top) */}
|
||||
{persons.length > 0 && (
|
||||
<View style={{ width: "100%", paddingHorizontal: 10, marginBottom: 12 }}>
|
||||
<Text style={{ color: "white", fontSize: 18, fontWeight: "600", marginBottom: 6 }}>Personen</Text>
|
||||
<View style={styles.sectionContainer}>
|
||||
<Text style={styles.sectionTitle}>Personen</Text>
|
||||
{persons.slice(0, 5).map((p) => (
|
||||
<PersonRow key={`p-${p.personId ?? p.id}`} person={p}/>
|
||||
))}
|
||||
@@ -214,8 +197,8 @@ export default function ExploreScreen() {
|
||||
)}
|
||||
|
||||
{/* Staffeln grouped by Show with page view */}
|
||||
<View style={{ width: "100%", paddingHorizontal: 10 }}>
|
||||
<Text style={{ color: "white", fontSize: 18, fontWeight: "600", marginBottom: 6 }}>Staffeln</Text>
|
||||
<View style={styles.sectionContainer}>
|
||||
<Text style={styles.sectionTitle}>Staffeln</Text>
|
||||
|
||||
{Array.from(seasonsByShowId.entries()).map(([showId, seasons]) => {
|
||||
const show = showsById[Number(showId)];
|
||||
@@ -242,7 +225,7 @@ export default function ExploreScreen() {
|
||||
})}
|
||||
|
||||
{seasonsByShowId.size === 0 && (
|
||||
<Text style={{ color: "white", fontSize: 16, textAlign: "center", marginTop: 14 }}>
|
||||
<Text style={styles.centerText}>
|
||||
Keine Staffeln gefunden. Passe deine Tags an.
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import styles from "@/app/tabStyles/indexStyles";
|
||||
import ShowCard from "@/components/ui/ShowCard";
|
||||
import { useShowContext } from "@/contexts/ShowContext";
|
||||
import { useStreamingServiceContext } from "@/contexts/StreamingServiceContext";
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import { useShows } from "@/hooks/useShows";
|
||||
import { useStreamingServices } from "@/hooks/useStreamingServices";
|
||||
import * as Haptics from "expo-haptics";
|
||||
import { router } from "expo-router";
|
||||
import React from "react";
|
||||
import {
|
||||
@@ -18,54 +18,43 @@ import {
|
||||
} from "react-native-gesture-handler";
|
||||
|
||||
export default function HomeScreen() {
|
||||
const { shows, error, loading } = useShowContext();
|
||||
const { streamingServices } = useStreamingServiceContext();
|
||||
const [filteredShows, setFilteredShows] = React.useState(shows);
|
||||
const { data: shows = [], error, isLoading: loading } = useShows();
|
||||
const { data: streamingServices = {} } = useStreamingServices();
|
||||
const [activeFilter, setActiveFilter] = React.useState<string>("all");
|
||||
|
||||
const haptikFeedback = () => {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
setFilteredShows(shows);
|
||||
}, [shows]);
|
||||
};
|
||||
|
||||
const handleFilter = (type: string) => {
|
||||
haptikFeedback();
|
||||
setActiveFilter(type);
|
||||
|
||||
|
||||
if (type === "all") {
|
||||
setFilteredShows(shows);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "live") {
|
||||
const filtered = shows.filter((show) => show.running);
|
||||
setFilteredShows(filtered);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === activeFilter) {
|
||||
setFilteredShows(shows);
|
||||
setActiveFilter('all');
|
||||
return;
|
||||
setActiveFilter("all");
|
||||
} else {
|
||||
setActiveFilter(type);
|
||||
}
|
||||
|
||||
|
||||
|
||||
const filtered = shows.filter((show) =>
|
||||
show.streamingService.split(',').map(s => s.trim()).includes(type)
|
||||
);
|
||||
setFilteredShows(filtered);
|
||||
};
|
||||
|
||||
const filteredShows = React.useMemo(() => {
|
||||
if (activeFilter === "all") {
|
||||
return shows;
|
||||
}
|
||||
if (activeFilter === "live") {
|
||||
return shows.filter((show) => show.running);
|
||||
}
|
||||
return shows.filter((show) =>
|
||||
show.streamingService
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.includes(activeFilter)
|
||||
);
|
||||
}, [shows, activeFilter]);
|
||||
|
||||
const uniqueStreamingServices = React.useMemo(() => {
|
||||
const uniqueServices = new Set<string>();
|
||||
shows.forEach((show) => {
|
||||
const services = show.streamingService.split(', ').map(s => s.trim());
|
||||
services.forEach(service => uniqueServices.add(service));
|
||||
const services = show.streamingService.split(", ").map((s) => s.trim());
|
||||
services.forEach((service) => uniqueServices.add(service));
|
||||
});
|
||||
return Array.from(uniqueServices);
|
||||
}, [shows]);
|
||||
@@ -91,7 +80,7 @@ export default function HomeScreen() {
|
||||
{ justifyContent: "center", alignItems: "center" },
|
||||
]}
|
||||
>
|
||||
<Text>Error: {error}</Text>
|
||||
<Text>Error: {error?.message || String(error)}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -102,7 +91,10 @@ export default function HomeScreen() {
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>FLTR</Text>
|
||||
</View>
|
||||
<ScrollView contentContainerStyle={{ paddingBottom: 30 }}>
|
||||
<ScrollView
|
||||
contentContainerStyle={{ paddingBottom: 30 }}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
>
|
||||
<View style={styles.filterSection}>
|
||||
<ScrollView
|
||||
horizontal
|
||||
@@ -127,7 +119,9 @@ export default function HomeScreen() {
|
||||
}}
|
||||
onPress={() => handleFilter("all")}
|
||||
>
|
||||
<Text style={{fontWeight: 'bold', color: 'white'}}>ALLE</Text>
|
||||
<Text style={{ fontWeight: "bold", color: "white" }}>
|
||||
ALLE
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{activeFilter !== "live" && (
|
||||
@@ -143,20 +137,30 @@ export default function HomeScreen() {
|
||||
}}
|
||||
onPress={() => handleFilter("live")}
|
||||
>
|
||||
<View style={{backgroundColor: "red", paddingHorizontal: 5, paddingVertical: 2, borderRadius: 5}}>
|
||||
<Text style={{fontWeight: 'bold', color: 'white'}}>LIVE</Text>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "red",
|
||||
paddingHorizontal: 5,
|
||||
paddingVertical: 2,
|
||||
borderRadius: 5,
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontWeight: "bold", color: "white" }}>
|
||||
LIVE
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
<View style={{
|
||||
<View
|
||||
style={{
|
||||
height: 60,
|
||||
width: 2,
|
||||
backgroundColor: "hsla(0, 0%, 37%, 1.00)",
|
||||
marginHorizontal: 5,
|
||||
borderRadius: 5,
|
||||
|
||||
}} />
|
||||
}}
|
||||
/>
|
||||
|
||||
{uniqueStreamingServices.map((serviceName) => {
|
||||
const streamingService =
|
||||
@@ -214,7 +218,14 @@ export default function HomeScreen() {
|
||||
})
|
||||
}
|
||||
imageUri={show.bannerUri}
|
||||
streamingServicesUris={show.streamingService.split(', ').map(s => streamingServices[`assets.images.streamingServices.${s.toLowerCase()}`])}
|
||||
streamingServicesUris={show.streamingService
|
||||
.split(", ")
|
||||
.map(
|
||||
(s) =>
|
||||
streamingServices[
|
||||
`assets.images.streamingServices.${s.toLowerCase()}`
|
||||
]
|
||||
)}
|
||||
genres={show.genres}
|
||||
{...(showLiveBadge
|
||||
? {
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { DiscoveryProvider } from "@/contexts/DiscoveryContext";
|
||||
import { SeasonProvider } from "@/contexts/SeasonContext";
|
||||
import { ShowProvider } from "@/contexts/ShowContext";
|
||||
import { StreamingServiceProvider } from "@/contexts/StreamingServiceContext";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { Stack } from "expo-router";
|
||||
import "react-native-reanimated";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
export default function RootLayout() {
|
||||
return (
|
||||
<ShowProvider>
|
||||
<SeasonProvider>
|
||||
<StreamingServiceProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<DiscoveryProvider>
|
||||
<Stack>
|
||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||
@@ -27,8 +25,6 @@ export default function RootLayout() {
|
||||
/>
|
||||
</Stack>
|
||||
</DiscoveryProvider>
|
||||
</StreamingServiceProvider>
|
||||
</SeasonProvider>
|
||||
</ShowProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { getPersonHistory, type PersonMini } from "@/apis/personHistoryApi";
|
||||
import { getShowById } from "@/apis/showApi";
|
||||
import { PersonMini } from "@/apis/personHistoryApi";
|
||||
import styles from "@/app/stackStyles/participantStyles";
|
||||
import { useShowContext } from "@/contexts/ShowContext";
|
||||
import { usePersonHistory, AppearanceGroup } from "@/hooks/usePersonHistory";
|
||||
import Ionicons from "@expo/vector-icons/Ionicons";
|
||||
import { router, useLocalSearchParams } from "expo-router";
|
||||
import * as WebBrowser from "expo-web-browser";
|
||||
@@ -12,35 +11,14 @@ import {
|
||||
ScrollView,
|
||||
} from "react-native-gesture-handler";
|
||||
|
||||
type SeasonEntry = {
|
||||
seasonNumber: number;
|
||||
partner: PersonMini | null;
|
||||
participants: PersonMini[];
|
||||
startDate: string | null;
|
||||
};
|
||||
|
||||
type AppearanceGroup = {
|
||||
show: {
|
||||
id: number;
|
||||
title: string;
|
||||
bannerUri: string;
|
||||
thumbnailUri: string;
|
||||
};
|
||||
seasons: SeasonEntry[];
|
||||
};
|
||||
|
||||
export default function ParticipantScreen() {
|
||||
const { shows } = useShowContext();
|
||||
const { name, participantId } = useLocalSearchParams();
|
||||
|
||||
const pid = Array.isArray(participantId)
|
||||
? Number(participantId[0])
|
||||
: Number(participantId);
|
||||
|
||||
const [, setLoading] = React.useState(false);
|
||||
const [, setError] = React.useState<string | null>(null);
|
||||
|
||||
const [appearances, setAppearances] = React.useState<AppearanceGroup[]>([]);
|
||||
const { data: appearances = [], isLoading, isError } = usePersonHistory(pid);
|
||||
|
||||
const formatYear = (iso?: string | null) => {
|
||||
if (!iso) return null;
|
||||
@@ -48,104 +26,6 @@ export default function ParticipantScreen() {
|
||||
return y || null;
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!pid || Number.isNaN(pid)) return;
|
||||
const controller = new AbortController();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const hist = await getPersonHistory(pid, controller.signal);
|
||||
|
||||
const grouped = new Map<number, Map<number, SeasonEntry>>();
|
||||
for (const h of hist) {
|
||||
if (!Number.isFinite(h.showId) || h.showId <= 0) continue;
|
||||
const seasonsForShow =
|
||||
grouped.get(h.showId) ?? new Map<number, SeasonEntry>();
|
||||
|
||||
const existing = seasonsForShow.get(h.seasonNumber);
|
||||
if (existing) {
|
||||
seasonsForShow.set(h.seasonNumber, {
|
||||
seasonNumber: h.seasonNumber,
|
||||
partner: existing.partner ?? h.partner ?? null,
|
||||
participants: existing.participants.length
|
||||
? existing.participants
|
||||
: (h.seasonParticipants ?? []),
|
||||
startDate: existing.startDate ?? h.startDate ?? null,
|
||||
});
|
||||
} else {
|
||||
seasonsForShow.set(h.seasonNumber, {
|
||||
seasonNumber: h.seasonNumber,
|
||||
partner: h.partner ?? null,
|
||||
participants: h.seasonParticipants ?? [],
|
||||
startDate: h.startDate ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
grouped.set(h.showId, seasonsForShow);
|
||||
}
|
||||
|
||||
const showIds = Array.from(grouped.keys());
|
||||
|
||||
const fromContext = showIds
|
||||
.map((id) => shows.find((s) => s.id === id))
|
||||
.filter((s): s is (typeof shows)[number] => !!s);
|
||||
|
||||
const missingIds = showIds.filter(
|
||||
(id) => !fromContext.some((s) => s.id === id)
|
||||
);
|
||||
|
||||
const fetched = await Promise.all(
|
||||
missingIds.map(async (id) => {
|
||||
try {
|
||||
const s = await getShowById(id);
|
||||
return s;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const allShows = [
|
||||
...fromContext,
|
||||
...fetched.filter(Boolean),
|
||||
] as typeof shows;
|
||||
|
||||
const result: AppearanceGroup[] = allShows.map((s) => {
|
||||
const seasonsMap = grouped.get(s.id)!;
|
||||
const seasonsSorted = Array.from(seasonsMap.values()).sort(
|
||||
(a, b) => a.seasonNumber - b.seasonNumber
|
||||
);
|
||||
return {
|
||||
show: {
|
||||
id: s.id,
|
||||
title: s.title,
|
||||
bannerUri: s.bannerUri,
|
||||
thumbnailUri: s.thumbnailUri,
|
||||
},
|
||||
seasons: seasonsSorted,
|
||||
};
|
||||
});
|
||||
|
||||
result.sort((a, b) =>
|
||||
a.show.title.localeCompare(b.show.title, "de", {
|
||||
sensitivity: "base",
|
||||
})
|
||||
);
|
||||
|
||||
setAppearances(result);
|
||||
} catch (e: any) {
|
||||
if (!controller.signal.aborted)
|
||||
setError(e?.message || "Fehler beim Laden");
|
||||
} finally {
|
||||
if (!controller.signal.aborted) setLoading(false);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => controller.abort();
|
||||
}, [pid, shows]);
|
||||
|
||||
const [expandedShows, setExpandedShows] = React.useState<Set<number>>(
|
||||
new Set()
|
||||
);
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { getShowById, Show } from "@/apis/showApi";
|
||||
|
||||
import ParticipantDetails from "@/components/ui/ParticipantDeatails";
|
||||
|
||||
import ShowInfo from "@/components/ui/ShowInfo";
|
||||
import StackHeader from "@/components/ui/StackHeader";
|
||||
import { useSeasonContext } from "@/contexts/SeasonContext";
|
||||
import {
|
||||
useSeasonCount,
|
||||
useSeasonDates,
|
||||
useSeasonParticipants,
|
||||
} from "@/hooks/useSeason";
|
||||
import { useShow } from "@/hooks/useShow";
|
||||
import * as Haptics from "expo-haptics";
|
||||
import { router, useLocalSearchParams } from "expo-router";
|
||||
import React, { useState } from "react";
|
||||
import React from "react";
|
||||
import {
|
||||
Dimensions,
|
||||
Image,
|
||||
@@ -19,93 +21,34 @@ import {
|
||||
import styles from "./stackStyles/showDetailStyles";
|
||||
|
||||
export default function ShowDetails() {
|
||||
const {
|
||||
// bannerUri,
|
||||
// description,
|
||||
// concept,
|
||||
// genres,
|
||||
// streamingService,
|
||||
id,
|
||||
// endDate,
|
||||
} = useLocalSearchParams();
|
||||
|
||||
const [show, setShow] = useState<Show | null>(null);
|
||||
const [, setLoading] = useState(true);
|
||||
const { id } = useLocalSearchParams();
|
||||
const showId = Number(id);
|
||||
|
||||
const [selectedParticipants, setSelectedParticipants] =
|
||||
React.useState<boolean>(true);
|
||||
const [selectedSeason, setSelectedSeason] = React.useState<number>(1);
|
||||
const showId = Number(id);
|
||||
const { fetchSeasonParticipants, fetchSeasonCount, fetchSeasonDates } =
|
||||
useSeasonContext();
|
||||
const [seasonCount, setSeasonCount] = React.useState<number>(0);
|
||||
const [participants, setParticipants] = React.useState<
|
||||
{ id: number; name: string; imageUri: string }[]
|
||||
>([]);
|
||||
const [startDate, setStartDate] = React.useState<string | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [pLoading, setPLoading] = React.useState(false);
|
||||
const [pError, setPError] = React.useState<string | null>(null);
|
||||
|
||||
const { data: show } = useShow(showId);
|
||||
const { data: seasonCount = 0 } = useSeasonCount(showId);
|
||||
const {
|
||||
data: participants,
|
||||
isLoading: pLoading,
|
||||
isError: pError,
|
||||
} = useSeasonParticipants(showId, selectedSeason);
|
||||
const { data: dates } = useSeasonDates(showId, selectedSeason);
|
||||
const startDate = dates?.startDate;
|
||||
|
||||
const sortedParticipants = React.useMemo(() => {
|
||||
return participants.sort((a, b) =>
|
||||
return [...participants].sort((a, b) =>
|
||||
a.name.localeCompare(b.name, "de", { sensitivity: "base" })
|
||||
);
|
||||
}, [participants]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!showId) return;
|
||||
|
||||
const fetchShow = async () => {
|
||||
try {
|
||||
const data = await getShowById(showId);
|
||||
setShow(data);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (seasonCount > 0 && selectedSeason > seasonCount) {
|
||||
setSelectedSeason(1);
|
||||
}
|
||||
};
|
||||
|
||||
fetchShow();
|
||||
|
||||
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, selectedSeason]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!showId || !selectedSeason) return;
|
||||
let active = true;
|
||||
(async () => {
|
||||
setPError(null);
|
||||
setPLoading(true);
|
||||
try {
|
||||
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 {
|
||||
if (active) setPLoading(false);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [showId, selectedSeason, fetchSeasonParticipants, fetchSeasonDates]);
|
||||
}, [seasonCount, selectedSeason]);
|
||||
|
||||
const formattedStartDate = React.useMemo(() => {
|
||||
if (!startDate) return "";
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Dimensions, StyleSheet } from "react-native";
|
||||
import { Colors } from "@/constants/colors";
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
mainContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: "hsl(221, 39%, 12%)",
|
||||
backgroundColor: Colors.header,
|
||||
},
|
||||
closeIcon: {
|
||||
position: "absolute",
|
||||
@@ -11,7 +12,7 @@ const styles = StyleSheet.create({
|
||||
right: 15,
|
||||
},
|
||||
participantName: {
|
||||
color: "white",
|
||||
color: Colors.text,
|
||||
fontSize: 20,
|
||||
fontWeight: "600",
|
||||
textAlign: "center",
|
||||
@@ -27,7 +28,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
participantInfoSection: {
|
||||
width: "100%",
|
||||
height: "auto",
|
||||
minHeight: "auto",
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
@@ -35,7 +36,7 @@ const styles = StyleSheet.create({
|
||||
marginTop: 5,
|
||||
},
|
||||
participantInfo: {
|
||||
color: "hsl(0, 0%, 80%)",
|
||||
color: Colors.textSecondary,
|
||||
fontSize: 16,
|
||||
textAlign: "center",
|
||||
},
|
||||
@@ -43,20 +44,20 @@ const styles = StyleSheet.create({
|
||||
width: 4,
|
||||
height: 4,
|
||||
borderRadius: 3,
|
||||
backgroundColor: "hsl(0, 0%, 80%)",
|
||||
backgroundColor: Colors.textSecondary,
|
||||
marginHorizontal: 7,
|
||||
marginTop: 2,
|
||||
},
|
||||
performedShowsSection: {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: 'hsl(221, 39%, 16%)',
|
||||
backgroundColor: Colors.background,
|
||||
marginTop: 20,
|
||||
},
|
||||
performedShowsTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
color: "hsl(0, 0%, 80%)",
|
||||
color: Colors.textSecondary,
|
||||
marginTop: 15,
|
||||
marginLeft: 15,
|
||||
},
|
||||
@@ -67,7 +68,7 @@ const styles = StyleSheet.create({
|
||||
borderRadius: 10,
|
||||
},
|
||||
showLabel: {
|
||||
color: "white",
|
||||
color: Colors.text,
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
textAlign: "center",
|
||||
@@ -83,14 +84,14 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: "#eee",
|
||||
},
|
||||
showTitle: {
|
||||
color: "white",
|
||||
color: Colors.text,
|
||||
fontSize: 12,
|
||||
fontWeight: "600",
|
||||
textAlign: "center",
|
||||
marginTop: 15,
|
||||
},
|
||||
showSeason: {
|
||||
color: "hsl(0, 0%, 80%)",
|
||||
color: Colors.textSecondary,
|
||||
fontSize: 12,
|
||||
fontWeight: "400",
|
||||
textAlign: "center",
|
||||
@@ -102,7 +103,7 @@ const styles = StyleSheet.create({
|
||||
borderRadius: 15,
|
||||
marginTop: 20,
|
||||
alignItems: "center",
|
||||
backgroundColor: "hsl(336, 79%, 63%)",
|
||||
backgroundColor: Colors.primary,
|
||||
},
|
||||
|
||||
card: {
|
||||
@@ -113,12 +114,12 @@ const styles = StyleSheet.create({
|
||||
horizontalLine: {
|
||||
height: 50,
|
||||
width: 2,
|
||||
backgroundColor: "hsl(0, 0%, 70%)",
|
||||
backgroundColor: Colors.textSecondary,
|
||||
marginTop: 10,
|
||||
alignSelf: "center",
|
||||
},
|
||||
partnerLabel: {
|
||||
color: "hsl(0, 0%, 80%)",
|
||||
color: Colors.textSecondary,
|
||||
fontSize: 12,
|
||||
fontWeight: "400",
|
||||
textAlign: "center",
|
||||
@@ -126,17 +127,17 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
participantContainer: {
|
||||
width: "auto",
|
||||
height: "auto",
|
||||
minHeight: "auto",
|
||||
borderRadius: 15,
|
||||
marginTop: 15,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: "hsl(221, 39%, 12%)",
|
||||
backgroundColor: Colors.header,
|
||||
padding: 10,
|
||||
},
|
||||
|
||||
participantLabel: {
|
||||
color: "white",
|
||||
color: Colors.text,
|
||||
fontSize: 12,
|
||||
},
|
||||
participantRow: {
|
||||
@@ -167,7 +168,7 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: "hsl(221, 39%, 28%)",
|
||||
},
|
||||
moreChipText: {
|
||||
color: "white",
|
||||
color: Colors.text,
|
||||
fontSize: 11,
|
||||
fontWeight: "600",
|
||||
},
|
||||
@@ -175,7 +176,7 @@ const styles = StyleSheet.create({
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: 20,
|
||||
backgroundColor: "hsl(221, 39%, 12%)",
|
||||
backgroundColor: Colors.header,
|
||||
marginLeft: 15,
|
||||
marginTop: 15,
|
||||
marginBottom: 5,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { StyleSheet } from "react-native";
|
||||
import { Colors } from "@/constants/colors";
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
mainContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: "hsl(221, 39%, 12%)",
|
||||
backgroundColor: Colors.header,
|
||||
},
|
||||
showImage: {
|
||||
width: 200,
|
||||
@@ -22,14 +23,14 @@ const styles = StyleSheet.create({
|
||||
bottom: 25,
|
||||
},
|
||||
showInfoText: {
|
||||
color: "hsl(0, 0%, 80%)",
|
||||
color: Colors.textSecondary,
|
||||
fontSize: 14,
|
||||
},
|
||||
dot: {
|
||||
width: 4,
|
||||
height: 4,
|
||||
borderRadius: 3,
|
||||
backgroundColor: "hsl(0, 0%, 80%)",
|
||||
backgroundColor: Colors.textSecondary,
|
||||
marginHorizontal: 7,
|
||||
marginTop: 2,
|
||||
},
|
||||
@@ -49,27 +50,27 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
infoContainner: {
|
||||
width: "100%",
|
||||
height: "auto",
|
||||
minHeight: "auto",
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 15,
|
||||
backgroundColor: "hsl(221, 39%, 0%)",
|
||||
backgroundColor: Colors.background,
|
||||
flexDirection: "row",
|
||||
gap: 20,
|
||||
},
|
||||
infoLabel: {
|
||||
fontWeight: "300",
|
||||
color: "hsl(0, 0%, 80%)",
|
||||
color: Colors.textSecondary,
|
||||
fontSize: 16,
|
||||
},
|
||||
participantsDetailsContainer: {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: "hsl(221, 39%, 2%)",
|
||||
backgroundColor: Colors.card,
|
||||
},
|
||||
participantContainer: {
|
||||
height: 160,
|
||||
width: 110,
|
||||
backgroundColor: "hsl(336, 79%, 63%)",
|
||||
backgroundColor: Colors.primary,
|
||||
borderRadius: 10,
|
||||
marginBottom: 30,
|
||||
},
|
||||
@@ -82,8 +83,8 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
seasonsSection: {
|
||||
width: "100%",
|
||||
height: 40,
|
||||
backgroundColor: "hsl(221, 39%, 2%)",
|
||||
minHeight: 40,
|
||||
backgroundColor: Colors.card,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
@@ -105,23 +106,23 @@ const styles = StyleSheet.create({
|
||||
alignItems: "center",
|
||||
},
|
||||
seasonLabel: {
|
||||
color: "white",
|
||||
color: Colors.text,
|
||||
fontWeight: "bold",
|
||||
},
|
||||
participantLabel: {
|
||||
color: "white",
|
||||
color: Colors.text,
|
||||
fontWeight: "500",
|
||||
textAlign: "center",
|
||||
fontSize: 11,
|
||||
marginTop: 10,
|
||||
},
|
||||
seasonsLabel: {
|
||||
color: "hsl(0, 0%, 80%)",
|
||||
color: Colors.textSecondary,
|
||||
fontWeight: "500",
|
||||
fontSize: 16,
|
||||
},
|
||||
detailTitle: {
|
||||
color: "hsl(0, 0%, 100%)",
|
||||
color: Colors.text,
|
||||
fontSize: 14,
|
||||
fontWeight: "bold",
|
||||
marginTop: 10,
|
||||
@@ -129,7 +130,7 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 5,
|
||||
},
|
||||
detailLabel: {
|
||||
color: "hsl(0, 0%, 80%)",
|
||||
color: Colors.textSecondary,
|
||||
fontSize: 14,
|
||||
lineHeight: 20,
|
||||
width: "90%",
|
||||
@@ -138,7 +139,7 @@ const styles = StyleSheet.create({
|
||||
marginTop: 5,
|
||||
},
|
||||
startDate: {
|
||||
color: "hsl(0, 0%, 80%)",
|
||||
color: Colors.textSecondary,
|
||||
fontSize: 16,
|
||||
textAlign: "center",
|
||||
marginTop: 15,
|
||||
|
||||
@@ -1,29 +1,36 @@
|
||||
import { Colors } from "@/constants/colors";
|
||||
import { StyleSheet } from "react-native";
|
||||
|
||||
export default StyleSheet.create({
|
||||
mainContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: "hsl(221, 39%, 11%)",
|
||||
// paddingHorizontal: 10,
|
||||
},
|
||||
header: {
|
||||
height: 125,
|
||||
backgroundColor: "hsl(221, 39%, 12%)",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "hsl(221, 39%, 15%)",
|
||||
const shadow = {
|
||||
shadowColor: "#000",
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 3,
|
||||
height: 2,
|
||||
},
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 3.84,
|
||||
elevation: 5,
|
||||
};
|
||||
|
||||
export default StyleSheet.create({
|
||||
mainContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: Colors.background,
|
||||
paddingHorizontal: 15,
|
||||
},
|
||||
header: {
|
||||
minHeight: 125,
|
||||
backgroundColor: Colors.header,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderBottomWidth: 1,
|
||||
borderRadius: 25,
|
||||
marginBottom: 15,
|
||||
borderBottomColor: Colors.border,
|
||||
...shadow,
|
||||
},
|
||||
title: {
|
||||
color: "white",
|
||||
color: Colors.text,
|
||||
fontSize: 38,
|
||||
fontWeight: "bold",
|
||||
marginTop: "auto",
|
||||
@@ -33,38 +40,37 @@ export default StyleSheet.create({
|
||||
position: "absolute",
|
||||
top: 15,
|
||||
left: 20,
|
||||
backgroundColor: "red",
|
||||
backgroundColor: Colors.red,
|
||||
borderRadius: 10,
|
||||
paddingVertical: 5,
|
||||
paddingHorizontal: 10,
|
||||
},
|
||||
filterSection: {
|
||||
width: "100%",
|
||||
height: 70,
|
||||
minHeight: 70,
|
||||
marginTop: 20,
|
||||
},
|
||||
searchContainer: {
|
||||
width: "100%",
|
||||
height: 60,
|
||||
marginHorizontal: "auto",
|
||||
backgroundColor: "hsl(221, 39%, 8%)",
|
||||
backgroundColor: Colors.card,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
borderRadius: 20,
|
||||
paddingHorizontal: 20,
|
||||
|
||||
marginTop: 15,
|
||||
borderWidth: 1.5,
|
||||
borderColor: "hsl(221, 39%, 15%)",
|
||||
shadowColor: "#000",
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 2,
|
||||
borderColor: Colors.border,
|
||||
...shadow,
|
||||
},
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 3.84,
|
||||
elevation: 5,
|
||||
searchInput: {
|
||||
fontSize: 18,
|
||||
fontWeight: "500",
|
||||
color: "hsl(221, 39%, 80%)",
|
||||
width: "90%",
|
||||
height: "100%",
|
||||
},
|
||||
searchLabel: {
|
||||
color: "hsl(221, 39%, 80%)",
|
||||
@@ -79,24 +85,16 @@ export default StyleSheet.create({
|
||||
height: "auto",
|
||||
paddingBottom: 15,
|
||||
borderRadius: 20,
|
||||
backgroundColor: "hsl(221, 39%, 8%)",
|
||||
backgroundColor: Colors.card,
|
||||
borderWidth: 1.5,
|
||||
borderColor: "hsl(221, 39%, 15%)",
|
||||
borderColor: Colors.border,
|
||||
marginHorizontal: "auto",
|
||||
alignSelf: "center",
|
||||
marginTop: 15,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 2,
|
||||
},
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 3.84,
|
||||
elevation: 5,
|
||||
// opacity: 0.9,
|
||||
...shadow,
|
||||
},
|
||||
suggestionTitle: {
|
||||
color: "hsl(0, 0%, 60%)",
|
||||
color: Colors.textSecondary,
|
||||
fontSize: 14,
|
||||
marginLeft: 15,
|
||||
marginTop: 15,
|
||||
@@ -116,10 +114,10 @@ export default StyleSheet.create({
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
borderWidth: 1.5,
|
||||
borderColor: "hsl(0, 0%, 90%)",
|
||||
borderColor: Colors.text,
|
||||
},
|
||||
suggestionLabel: {
|
||||
color: "white",
|
||||
color: Colors.text,
|
||||
fontSize: 12,
|
||||
fontWeight: "500",
|
||||
marginLeft: 10,
|
||||
@@ -136,7 +134,7 @@ export default StyleSheet.create({
|
||||
marginTop: 5,
|
||||
},
|
||||
tagLabel: {
|
||||
color: "white",
|
||||
color: Colors.text,
|
||||
marginRight: 5,
|
||||
},
|
||||
tagContainer: {
|
||||
@@ -164,6 +162,23 @@ export default StyleSheet.create({
|
||||
justifyContent: "center",
|
||||
marginRight: 10,
|
||||
},
|
||||
personName: { color: "white", fontSize: 16, fontWeight: "600" },
|
||||
personName: { color: Colors.text, fontSize: 16, fontWeight: "600" },
|
||||
personMeta: { color: "#bbb", fontSize: 12, marginTop: 2 },
|
||||
sectionContainer: {
|
||||
width: "100%",
|
||||
paddingHorizontal: 10,
|
||||
marginBottom: 12,
|
||||
},
|
||||
sectionTitle: {
|
||||
color: Colors.text,
|
||||
fontSize: 18,
|
||||
fontWeight: "600",
|
||||
marginBottom: 6,
|
||||
},
|
||||
centerText: {
|
||||
color: Colors.text,
|
||||
fontSize: 16,
|
||||
textAlign: "center",
|
||||
marginTop: 14,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
export const Colors = {
|
||||
background: 'hsl(221, 39%, 11%)',
|
||||
header: 'hsl(221, 39%, 12%)',
|
||||
card: 'hsl(221, 39%, 8%)',
|
||||
border: 'hsl(221, 39%, 15%)',
|
||||
text: 'white',
|
||||
textSecondary: 'hsl(0, 0%, 60%)',
|
||||
primary: '#199edb',
|
||||
red: 'red',
|
||||
};
|
||||
|
||||
export type ShowDetailColors = {
|
||||
tabColor: string;
|
||||
seasonColor: string;
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
useCallback,
|
||||
} from "react";
|
||||
import { getAutoComplete, AutoCompleteItem } from "@/apis/autoCompleteApi";
|
||||
import { useAutoComplete } from "@/hooks/useAutoComplete";
|
||||
import { AutoCompleteItem } from "@/apis/autoCompleteApi";
|
||||
|
||||
type DiscoveryContextType = {
|
||||
query: string;
|
||||
@@ -25,61 +23,15 @@ export const DiscoveryProvider = ({
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const [query, setQuery] = useState("");
|
||||
const [suggestions, setSuggestions] = useState<AutoCompleteItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const cacheRef = useRef<Record<string, AutoCompleteItem[]>>({});
|
||||
|
||||
const fetchSuggestions = useCallback((q: string) => {
|
||||
if (abortRef.current) abortRef.current.abort();
|
||||
if (!q.trim()) {
|
||||
setSuggestions([]);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const cached = cacheRef.current[q];
|
||||
if (cached) {
|
||||
setSuggestions(cached);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const controller = new AbortController();
|
||||
abortRef.current = controller;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
getAutoComplete(q, 10, controller.signal)
|
||||
.then((items) => {
|
||||
cacheRef.current[q] = items;
|
||||
setSuggestions(items);
|
||||
})
|
||||
.catch((e) => {
|
||||
if (controller.signal.aborted) return;
|
||||
setError(e.message || "Fehler");
|
||||
})
|
||||
.finally(() => {
|
||||
if (!controller.signal.aborted) setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (debounceRef.current) clearTimeout(debounceRef.current);
|
||||
debounceRef.current = setTimeout(() => fetchSuggestions(query), 300);
|
||||
return () => {
|
||||
if (debounceRef.current) clearTimeout(debounceRef.current);
|
||||
};
|
||||
}, [query, fetchSuggestions]);
|
||||
const { data: suggestions = [], isLoading: loading, error } = useAutoComplete(query);
|
||||
|
||||
const clear = () => {
|
||||
setQuery("");
|
||||
setSuggestions([]);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<DiscoveryContext.Provider
|
||||
value={{ query, setQuery, suggestions, loading, error, clear }}
|
||||
value={{ query, setQuery, suggestions, loading, error: error?.message || null, clear }}
|
||||
>
|
||||
{children}
|
||||
</DiscoveryContext.Provider>
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import { getSeason, SeasonParticipant } from "@/apis/seasonApi";
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
type SeasonContextType = {
|
||||
fetchSeasonParticipants: (
|
||||
showId: number,
|
||||
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: 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) => {
|
||||
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]
|
||||
);
|
||||
|
||||
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, fetchSeasonDates }}
|
||||
>
|
||||
{children}
|
||||
</SeasonContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export const useSeasonContext = () => {
|
||||
const ctx = useContext(SeasonContext);
|
||||
if (!ctx)
|
||||
throw new Error("useSeasonContext must be used within a SeasonProvider");
|
||||
return ctx;
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
import { getShows, Show } from "@/apis/showApi";
|
||||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
|
||||
type ShowContextType = {
|
||||
shows: Show[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
};
|
||||
|
||||
const ShowContext = createContext<ShowContextType | null>(null);
|
||||
|
||||
export const ShowProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [shows, setShows] = useState<Show[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const data = await getShows();
|
||||
setShows(data);
|
||||
} catch {
|
||||
setError("Failed to fetch shows");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ShowContext.Provider value={{ shows, loading, error }}>
|
||||
{children}
|
||||
</ShowContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useShowContext = () => {
|
||||
const ctx = useContext(ShowContext);
|
||||
if (!ctx)
|
||||
throw new Error("useShowContext must be used within a ShowProvider");
|
||||
return ctx;
|
||||
};
|
||||
@@ -1,59 +0,0 @@
|
||||
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;
|
||||
};
|
||||
13
hooks/useAutoComplete.ts
Normal file
13
hooks/useAutoComplete.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getAutoComplete } from "@/apis/autoCompleteApi";
|
||||
import { useDebounce } from "./useDebounce";
|
||||
|
||||
export const useAutoComplete = (query: string) => {
|
||||
const debouncedQuery = useDebounce(query, 300);
|
||||
|
||||
return useQuery({
|
||||
queryKey: ["autoComplete", debouncedQuery],
|
||||
queryFn: () => getAutoComplete(debouncedQuery),
|
||||
enabled: !!debouncedQuery,
|
||||
});
|
||||
};
|
||||
17
hooks/useDebounce.ts
Normal file
17
hooks/useDebounce.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export const useDebounce = <T>(value: T, delay: number): T => {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value);
|
||||
}, delay);
|
||||
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
}, [value, delay]);
|
||||
|
||||
return debouncedValue;
|
||||
};
|
||||
87
hooks/usePersonHistory.ts
Normal file
87
hooks/usePersonHistory.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getPersonHistory, PersonHistoryRecord, PersonMini } from "@/apis/personHistoryApi";
|
||||
import { getShowById, Show } from "@/apis/showApi";
|
||||
|
||||
type SeasonEntry = {
|
||||
seasonNumber: number;
|
||||
partner: PersonMini | null;
|
||||
participants: PersonMini[];
|
||||
startDate: string | null;
|
||||
};
|
||||
|
||||
export type AppearanceGroup = {
|
||||
show: Show;
|
||||
seasons: SeasonEntry[];
|
||||
};
|
||||
|
||||
export const usePersonHistory = (personId: number) => {
|
||||
return useQuery({
|
||||
queryKey: ["personHistory", personId],
|
||||
queryFn: async () => {
|
||||
const history = await getPersonHistory(personId);
|
||||
|
||||
const grouped = new Map<number, Map<number, SeasonEntry>>();
|
||||
for (const h of history) {
|
||||
if (!Number.isFinite(h.showId) || h.showId <= 0) continue;
|
||||
const seasonsForShow =
|
||||
grouped.get(h.showId) ?? new Map<number, SeasonEntry>();
|
||||
|
||||
const existing = seasonsForShow.get(h.seasonNumber);
|
||||
if (existing) {
|
||||
seasonsForShow.set(h.seasonNumber, {
|
||||
seasonNumber: h.seasonNumber,
|
||||
partner: existing.partner ?? h.partner ?? null,
|
||||
participants: existing.participants.length
|
||||
? existing.participants
|
||||
: (h.seasonParticipants ?? []),
|
||||
startDate: existing.startDate ?? h.startDate ?? null,
|
||||
});
|
||||
} else {
|
||||
seasonsForShow.set(h.seasonNumber, {
|
||||
seasonNumber: h.seasonNumber,
|
||||
partner: h.partner ?? null,
|
||||
participants: h.seasonParticipants ?? [],
|
||||
startDate: h.startDate ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
grouped.set(h.showId, seasonsForShow);
|
||||
}
|
||||
|
||||
const showIds = Array.from(grouped.keys());
|
||||
|
||||
const shows = await Promise.all(
|
||||
showIds.map(async (id) => {
|
||||
try {
|
||||
const s = await getShowById(id);
|
||||
return s;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const validShows = shows.filter((s): s is Show => !!s);
|
||||
|
||||
const result: AppearanceGroup[] = validShows.map((s) => {
|
||||
const seasonsMap = grouped.get(s.id)!;
|
||||
const seasonsSorted = Array.from(seasonsMap.values()).sort(
|
||||
(a, b) => a.seasonNumber - b.seasonNumber
|
||||
);
|
||||
return {
|
||||
show: s,
|
||||
seasons: seasonsSorted,
|
||||
};
|
||||
});
|
||||
|
||||
result.sort((a, b) =>
|
||||
a.show.title.localeCompare(b.show.title, "de", {
|
||||
sensitivity: "base",
|
||||
})
|
||||
);
|
||||
|
||||
return result;
|
||||
},
|
||||
enabled: !!personId,
|
||||
});
|
||||
};
|
||||
10
hooks/useSearch.ts
Normal file
10
hooks/useSearch.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getSearchResults } from "@/apis/searchApi";
|
||||
|
||||
export const useSearch = (tags: string[]) => {
|
||||
return useQuery({
|
||||
queryKey: ["search", tags],
|
||||
queryFn: () => getSearchResults(tags),
|
||||
enabled: tags.length > 0,
|
||||
});
|
||||
};
|
||||
50
hooks/useSeason.ts
Normal file
50
hooks/useSeason.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getSeason } from "@/apis/seasonApi";
|
||||
|
||||
export const useSeason = (showId: number, seasonNumber: number) => {
|
||||
return useQuery({
|
||||
queryKey: ["season", showId, seasonNumber],
|
||||
queryFn: () => getSeason(showId, seasonNumber),
|
||||
enabled: !!showId && !!seasonNumber,
|
||||
});
|
||||
};
|
||||
|
||||
export const useSeasonParticipants = (showId: number, seasonNumber: number) => {
|
||||
const { data: season, ...rest } = useSeason(showId, seasonNumber);
|
||||
return {
|
||||
data: season?.participants ?? [],
|
||||
...rest,
|
||||
};
|
||||
};
|
||||
|
||||
export const useSeasonDates = (showId: number, seasonNumber: number) => {
|
||||
const { data: season, ...rest } = useSeason(showId, seasonNumber);
|
||||
return {
|
||||
data: season ? { startDate: season.startDate, endDate: season.endDate } : null,
|
||||
...rest,
|
||||
};
|
||||
};
|
||||
|
||||
// This is a bit tricky, as we need to fetch seasons sequentially to know the count.
|
||||
// React Query is not ideal for this kind of sequential fetching.
|
||||
// However, we can still wrap the existing logic in a useQuery hook.
|
||||
// This is not the most efficient way, but it's better than nothing.
|
||||
export const useSeasonCount = (showId: number) => {
|
||||
return useQuery({
|
||||
queryKey: ["seasonCount", showId],
|
||||
queryFn: async () => {
|
||||
let n = 0;
|
||||
for (let s = 1; s <= 50; s++) {
|
||||
try {
|
||||
const season = await getSeason(showId, s);
|
||||
if (!season) break;
|
||||
n = s;
|
||||
} catch {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
},
|
||||
enabled: !!showId,
|
||||
});
|
||||
};
|
||||
10
hooks/useShow.ts
Normal file
10
hooks/useShow.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getShowById } from "@/apis/showApi";
|
||||
|
||||
export const useShow = (showId: number) => {
|
||||
return useQuery({
|
||||
queryKey: ["show", showId],
|
||||
queryFn: () => getShowById(showId),
|
||||
enabled: !!showId,
|
||||
});
|
||||
};
|
||||
9
hooks/useShows.ts
Normal file
9
hooks/useShows.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getShows } from "@/apis/showApi";
|
||||
|
||||
export const useShows = () => {
|
||||
return useQuery({
|
||||
queryKey: ["shows"],
|
||||
queryFn: getShows,
|
||||
});
|
||||
};
|
||||
13
hooks/useStreamingServices.ts
Normal file
13
hooks/useStreamingServices.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getStreamingImages, StreamingServiceRaw } from "@/apis/streamingServiceApi";
|
||||
|
||||
export const useStreamingServices = () => {
|
||||
return useQuery({
|
||||
queryKey: ["streamingServices"],
|
||||
queryFn: async () => {
|
||||
const data: StreamingServiceRaw[] = await getStreamingImages();
|
||||
const mapped = Object.fromEntries(data.map((s) => [s.key, s.value]));
|
||||
return mapped;
|
||||
},
|
||||
});
|
||||
};
|
||||
27
package-lock.json
generated
27
package-lock.json
generated
@@ -14,6 +14,7 @@
|
||||
"@react-navigation/bottom-tabs": "^7.3.10",
|
||||
"@react-navigation/elements": "^2.3.8",
|
||||
"@react-navigation/native": "^7.1.6",
|
||||
"@tanstack/react-query": "^5.90.5",
|
||||
"expo": "54.0.21",
|
||||
"expo-blur": "~15.0.7",
|
||||
"expo-constants": "~18.0.10",
|
||||
@@ -3873,6 +3874,32 @@
|
||||
"@sinonjs/commons": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.90.5",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.5.tgz",
|
||||
"integrity": "sha512-wLamYp7FaDq6ZnNehypKI5fNvxHPfTYylE0m/ZpuuzJfJqhR5Pxg9gvGBHZx4n7J+V5Rg5mZxHHTlv25Zt5u+w==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query": {
|
||||
"version": "5.90.5",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.5.tgz",
|
||||
"integrity": "sha512-pN+8UWpxZkEJ/Rnnj2v2Sxpx1WFlaa9L6a4UO89p6tTQbeo+m0MS8oYDjbggrR8QcTyjKoYWKS3xJQGr3ExT8Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.90.5"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"@react-navigation/bottom-tabs": "^7.3.10",
|
||||
"@react-navigation/elements": "^2.3.8",
|
||||
"@react-navigation/native": "^7.1.6",
|
||||
"@tanstack/react-query": "^5.90.5",
|
||||
"expo": "54.0.21",
|
||||
"expo-blur": "~15.0.7",
|
||||
"expo-constants": "~18.0.10",
|
||||
|
||||
Reference in New Issue
Block a user