updated search
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
import { AutoCompleteItem } from "@/apis/autoCompleteApi";
|
import { AutoCompleteItem } from "@/apis/autoCompleteApi";
|
||||||
import { getSearchResults, SearchResultItem } from "@/apis/searchApi";
|
import { getSearchResults, SearchResultItem } from "@/apis/searchApi";
|
||||||
|
import { Season } from "@/apis/seasonApi";
|
||||||
|
import { Show } from "@/apis/showApi";
|
||||||
import styles from "@/app/tabStyles/indexStyles";
|
import styles from "@/app/tabStyles/indexStyles";
|
||||||
import { Season, Show } from "@/app/types";
|
|
||||||
import ShowBox from "@/components/discovery/ShowBox";
|
import ShowBox from "@/components/discovery/ShowBox";
|
||||||
import { useDiscoveryContext } from "@/contexts/DiscoveryContext";
|
import { useDiscoveryContext } from "@/contexts/DiscoveryContext";
|
||||||
import { FontAwesome } from "@expo/vector-icons";
|
import { FontAwesome } from "@expo/vector-icons";
|
||||||
@@ -56,6 +57,8 @@ export default function ExploreScreen() {
|
|||||||
}
|
}
|
||||||
}, [results]);
|
}, [results]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Group SEASON results by showId
|
// Group SEASON results by showId
|
||||||
const seasonsByShowId = React.useMemo(() => {
|
const seasonsByShowId = React.useMemo(() => {
|
||||||
const map = new Map<number, Season[]>();
|
const map = new Map<number, Season[]>();
|
||||||
@@ -84,24 +87,27 @@ export default function ExploreScreen() {
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const needed = Array.from(seasonsByShowId.keys()).filter((id) => !showsById[id]);
|
const needed = Array.from(seasonsByShowId.keys()).filter((id) => !showsById[id]);
|
||||||
if (needed.length === 0) return;
|
if (needed.length === 0) return;
|
||||||
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const fetched = await Promise.all(needed.map((id) => getShowById(id)));
|
const fetched = await Promise.all(needed.map((id) => getShowById(id)));
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
|
|
||||||
const next: Record<number, Show> = {};
|
const next: Record<number, Show> = {};
|
||||||
for (const s of fetched) {
|
for (const s of fetched) {
|
||||||
if (!s) continue;
|
if (!s) continue;
|
||||||
next[Number((s as any).showId)] = s as Show;
|
next[s.id] = s; // wichtig: s.id, nicht s.showId
|
||||||
|
}
|
||||||
|
if (Object.keys(next).length) {
|
||||||
|
setShowsById((prev) => ({ ...prev, ...next }));
|
||||||
}
|
}
|
||||||
if (Object.keys(next).length) setShowsById((prev) => ({ ...prev, ...next }));
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
return () => { cancelled = true; };
|
||||||
};
|
|
||||||
}, [seasonsByShowId, showsById]);
|
}, [seasonsByShowId, showsById]);
|
||||||
|
|
||||||
// PERSON hits shown at the top (like old screen)
|
// PERSON hits shown at the top (like old screen)
|
||||||
@@ -188,7 +194,7 @@ export default function ExploreScreen() {
|
|||||||
<View style={{ width: "100%", paddingHorizontal: 10, marginBottom: 12 }}>
|
<View style={{ width: "100%", paddingHorizontal: 10, marginBottom: 12 }}>
|
||||||
<Text style={{ color: "white", fontSize: 18, fontWeight: "600", marginBottom: 6 }}>Personen</Text>
|
<Text style={{ color: "white", fontSize: 18, fontWeight: "600", marginBottom: 6 }}>Personen</Text>
|
||||||
{persons.slice(0, 5).map((p) => (
|
{persons.slice(0, 5).map((p) => (
|
||||||
<PersonRow key={`p-${p.personId ?? p.id}`} person={p} />
|
<PersonRow key={`p-${p.personId ?? p.id}`} person={p}/>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@@ -203,21 +209,21 @@ export default function ExploreScreen() {
|
|||||||
// If show metadata is not yet loaded, render a minimal ShowBox fallback once per page item
|
// If show metadata is not yet loaded, render a minimal ShowBox fallback once per page item
|
||||||
if (!show) {
|
if (!show) {
|
||||||
return (
|
return (
|
||||||
<SeasonCarousel
|
<SeasonCarousel
|
||||||
key={`sc-${showId}`}
|
key={`sc-${showId}`}
|
||||||
show={{ showId: showId as any, title: "blaaa", description: "", genre: "", thumbnailUrl: "", running: false } as any}
|
show={{ showId: showId as any, title: "blaaa", description: "", genre: "", thumbnailUrl: "", running: false } as any}
|
||||||
seasons={seasons}
|
seasons={seasons}
|
||||||
renderItem={(s) => <ShowBox show={{ showId: showId as any, title: `Show #${showId}`, description: "", genre: "", thumbnailUrl: "", running: false } as any} displayedSeason={s} shadow={false} />}
|
renderItem={(s) => <ShowBox show={{ showId: showId as any, title: `Show #${showId}`, description: "", genre: "", thumbnailUrl: "", running: false } as any} displayedSeason={s} shadow={false} />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<SeasonCarousel
|
<SeasonCarousel
|
||||||
key={`sc-${showId}`}
|
key={`sc-${showId}`}
|
||||||
show={show}
|
show={show}
|
||||||
seasons={seasons}
|
seasons={seasons}
|
||||||
renderItem={(s) => <ShowBox show={show} displayedSeason={s} shadow={false} />}
|
renderItem={(s) => <ShowBox show={show} displayedSeason={s} shadow={false} />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
|||||||
32
app/types.ts
32
app/types.ts
@@ -1,32 +0,0 @@
|
|||||||
export type Person = {
|
|
||||||
personId: number;
|
|
||||||
name: string;
|
|
||||||
birthDate?: string | null;
|
|
||||||
imageUrl?: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export type Season = {
|
|
||||||
seasonId: number;
|
|
||||||
showId: number;
|
|
||||||
startDate?: string | null;
|
|
||||||
endDate?: string | null;
|
|
||||||
seasonNumber?: number | null;
|
|
||||||
participants?: Person[];
|
|
||||||
moderators?: Person[];
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export type Show = {
|
|
||||||
showId: number;
|
|
||||||
title: string;
|
|
||||||
description?: string;
|
|
||||||
genre?: string;
|
|
||||||
thumbnailUrl: string;
|
|
||||||
logoUrl?: string;
|
|
||||||
bannerUrl?: string;
|
|
||||||
running?: boolean;
|
|
||||||
streamingServices?: string;
|
|
||||||
concept?: string
|
|
||||||
};
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { FontAwesome } from "@expo/vector-icons";
|
import { FontAwesome } from "@expo/vector-icons";
|
||||||
import { useNavigation } from "@react-navigation/native";
|
import { useNavigation } from "@react-navigation/native";
|
||||||
|
import { router } from "expo-router";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
||||||
|
|
||||||
@@ -16,16 +17,32 @@ const calcAge = (birthDate?: string | null): number | null => {
|
|||||||
return age < 0 || age > 130 ? null : age;
|
return age < 0 || age > 130 ? null : age;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function PersonRow({ person }: { person: PersonLite }) {
|
type Props = {
|
||||||
|
person: any;
|
||||||
|
onPress?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default function PersonRow({ person, onPress }: Props) {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
const age = calcAge(person.birthDate);
|
const age = calcAge(person.birthDate);
|
||||||
const id = person.personId ?? person.id;
|
const id = person.personId ?? person.id;
|
||||||
|
|
||||||
|
const goToPerson = React.useCallback(
|
||||||
|
(id: number) => {
|
||||||
|
console.log("go to person", id);
|
||||||
|
router.push({
|
||||||
|
pathname: "/participant",
|
||||||
|
params: { participantId: String(id), name: person.name },
|
||||||
|
});
|
||||||
|
}, []
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
// If your PersonDetail expects a Person object instead of an id, adapt this accordingly
|
goToPerson(Number(id));
|
||||||
// navigation.navigate("PersonDetail" as never, { personId: id } as never);
|
|
||||||
}}
|
}}
|
||||||
style={styles.personRow}
|
style={styles.personRow}
|
||||||
>
|
>
|
||||||
@@ -34,7 +51,7 @@ export default function PersonRow({ person }: { person: PersonLite }) {
|
|||||||
</View>
|
</View>
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
<Text style={styles.personName}>{person.name || "Unbekannt"}{age != null ? ` (${age})` : ""}</Text>
|
<Text style={styles.personName}>{person.name || "Unbekannt"}{age != null ? ` (${age})` : ""}</Text>
|
||||||
<Text style={styles.personMeta}>aus: unterschiedlichen Shows</Text>
|
{/* <Text style={styles.personMeta}>aus: unterschiedlichen Shows</Text> */}
|
||||||
</View>
|
</View>
|
||||||
<FontAwesome name="chevron-right" size={14} color="#888" />
|
<FontAwesome name="chevron-right" size={14} color="#888" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Season, Show } from "@/app/types";
|
import { Season } from "@/apis/seasonApi";
|
||||||
|
import { Show } from "@/apis/showApi";
|
||||||
import { FontAwesome } from "@expo/vector-icons";
|
import { FontAwesome } from "@expo/vector-icons";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Dimensions, FlatList, LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent, Pressable, StyleSheet, View } from "react-native";
|
import { Dimensions, FlatList, LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent, Pressable, StyleSheet, View } from "react-native";
|
||||||
@@ -18,7 +19,7 @@ export default function SeasonCarousel({
|
|||||||
}) {
|
}) {
|
||||||
const [currentIndex, setCurrentIndex] = React.useState(0);
|
const [currentIndex, setCurrentIndex] = React.useState(0);
|
||||||
const [sliderWidth, setSliderWidth] = React.useState(Math.floor(WINDOW_WIDTH - 20));
|
const [sliderWidth, setSliderWidth] = React.useState(Math.floor(WINDOW_WIDTH - 20));
|
||||||
const listRef = React.useRef<FlatList<Season> | null>(null);
|
const listRef = React.useRef<FlatList<Season>>(null);
|
||||||
|
|
||||||
const onLayout = (e: LayoutChangeEvent) => {
|
const onLayout = (e: LayoutChangeEvent) => {
|
||||||
const w = Math.max(0, Math.floor(e.nativeEvent.layout.width));
|
const w = Math.max(0, Math.floor(e.nativeEvent.layout.width));
|
||||||
@@ -47,9 +48,9 @@ export default function SeasonCarousel({
|
|||||||
return (
|
return (
|
||||||
<View style={{ marginBottom: 10, backgroundColor: "#1b1e2b", borderRadius: 10 }} onLayout={onLayout}>
|
<View style={{ marginBottom: 10, backgroundColor: "#1b1e2b", borderRadius: 10 }} onLayout={onLayout}>
|
||||||
<FlatList
|
<FlatList
|
||||||
ref={(r) => (listRef.current = r)}
|
ref={listRef}
|
||||||
data={seasons}
|
data={seasons}
|
||||||
keyExtractor={(season, idx) => `${show.showId}-${(season as any)?.seasonId ?? `season-${idx}`}`}
|
keyExtractor={(season, idx) => `${show.id}-${(season as any)?.seasonId ?? `season-${idx}`}`}
|
||||||
horizontal
|
horizontal
|
||||||
pagingEnabled
|
pagingEnabled
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
@@ -71,7 +72,7 @@ export default function SeasonCarousel({
|
|||||||
|
|
||||||
<View style={carouselStyles.dotsRow}>
|
<View style={carouselStyles.dotsRow}>
|
||||||
{seasons.map((_, i) => (
|
{seasons.map((_, i) => (
|
||||||
<View key={`dot-${show.showId}-${i}`} style={[carouselStyles.dot, i === currentIndex && carouselStyles.dotActive]} />
|
<View key={`dot-${show.id}-${i}`} style={[carouselStyles.dot, i === currentIndex && carouselStyles.dotActive]} />
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import { Season, Show } from "@/app/types";
|
import { Season } from "@/apis/seasonApi";
|
||||||
import GenreTag from "@/components/discovery/GenreTag";
|
import { Show } from "@/apis/showApi";
|
||||||
import { useNavigation } from "@react-navigation/native";
|
import { useNavigation } from "@react-navigation/native";
|
||||||
|
import { router } from "expo-router";
|
||||||
|
import React from "react";
|
||||||
import { Image, StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
import { Image, StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default function ShowBox({
|
export default function ShowBox({
|
||||||
show,
|
show,
|
||||||
displayedSeason,
|
displayedSeason,
|
||||||
@@ -14,9 +19,14 @@ export default function ShowBox({
|
|||||||
}) {
|
}) {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
|
|
||||||
|
const goToShow = React.useCallback((id: number) => {
|
||||||
|
router.push({ pathname: "/showDetails", params: { id: String(id) } });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => navigation.navigate("ShowDetail", { show })}
|
onPress={() => goToShow(Number(show.id))}
|
||||||
style={
|
style={
|
||||||
!shadow
|
!shadow
|
||||||
? [styles.showContainer, { backgroundColor: "#1b1e2b", paddingBottom: 0 }]
|
? [styles.showContainer, { backgroundColor: "#1b1e2b", paddingBottom: 0 }]
|
||||||
@@ -24,7 +34,7 @@ export default function ShowBox({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<View style={styles.showImageContainer}>
|
<View style={styles.showImageContainer}>
|
||||||
<Image source={{ uri: show.thumbnailUrl }} style={styles.showImage} />
|
<Image source={{ uri: show.thumbnailUri }} style={styles.showImage} />
|
||||||
{show.running && <Text style={styles.runningTag}>LIVE</Text>}
|
{show.running && <Text style={styles.runningTag}>LIVE</Text>}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -33,19 +43,22 @@ export default function ShowBox({
|
|||||||
|
|
||||||
{displayedSeason ? (
|
{displayedSeason ? (
|
||||||
<Text style={{ fontWeight: "bold", color: "#aac0ce" }}>
|
<Text style={{ fontWeight: "bold", color: "#aac0ce" }}>
|
||||||
Staffel {displayedSeason.seasonNumber} (
|
Staffel {displayedSeason.seasonNumber}
|
||||||
{new Date(displayedSeason.startDate).getFullYear()})
|
{displayedSeason.startDate
|
||||||
|
? ` (${new Date(displayedSeason.startDate).getFullYear()})`
|
||||||
|
: ""}
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|
||||||
<Text style={styles.showDescription} numberOfLines={8} ellipsizeMode="tail">
|
<Text style={styles.showDescription} numberOfLines={8} ellipsizeMode="tail">
|
||||||
{show.description}
|
{show.description}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<View style={styles.showGenreTagContainer}>
|
<View style={styles.showGenreTagContainer}>
|
||||||
{show.genre.split(", ").map((genre: any) => (
|
{/* {show.genres.map((genre: string) => (
|
||||||
<GenreTag key={genre}>{genre}</GenreTag>
|
<GenreTag key={genre}>{genre}</GenreTag>
|
||||||
))}
|
))} */}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ export const getIconName = (type: AutoCompleteItem["type"]) => {
|
|||||||
case "SHOW":
|
case "SHOW":
|
||||||
return "television";
|
return "television";
|
||||||
case "YEAR":
|
case "YEAR":
|
||||||
case "CUSTOM":
|
|
||||||
return "calendar";
|
return "calendar";
|
||||||
|
case "CUSTOM":
|
||||||
|
return "tag";
|
||||||
default:
|
default:
|
||||||
return "tag";
|
return "tag";
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user