import { AutoCompleteItem } from "@/apis/autoCompleteApi"; import { Season } from "@/apis/seasonApi"; import { Show } from "@/apis/showApi"; import styles from "@/app/tabStyles/indexStyles"; import ShowBox from "@/components/discovery/ShowBox"; import { useDiscoveryContext } from "@/contexts/DiscoveryContext"; import { FontAwesome } from "@expo/vector-icons"; import Feather from "@expo/vector-icons/Feather"; import React from "react"; import { Keyboard, ScrollView, Text, TextInput, TouchableOpacity, TouchableWithoutFeedback, View, } from "react-native"; import { getShowById } from "@/apis/showApi"; import PersonRow from "@/components/discovery/PersonRow"; import SeasonCarousel from "@/components/discovery/SeasonCarousel"; import TagChip from "@/components/discovery/TagChip"; import { useSearch } from "@/hooks/useSearch"; import { getIconName, mapApiPersonToUI, mapApiSeasonToUI, mapApiShowToUI, } from "@/utils/searchMapping"; export default function ExploreScreen() { const { query, setQuery, suggestions } = useDiscoveryContext(); const [tags, setTags] = React.useState([]); const tagStrings = React.useMemo(() => tags.map((t) => t.text), [tags]); const { data: results = [], error, refetch, } = useSearch(tagStrings as string[]); // Lokaler Show-Cache const [showsById, setShowsById] = React.useState>({}); // Steuerung für Vorschlagsliste const [showSuggestions, setShowSuggestions] = React.useState(false); function tagAdded(tag: AutoCompleteItem) { const nextTags = tags.some((t) => t.text === tag.text) ? tags : [...tags, tag]; setTags(nextTags); setQuery(""); setShowSuggestions(false); Keyboard.dismiss(); } function tagRemoved(tag: AutoCompleteItem) { const nextTags = tags.filter((t) => t.text !== tag.text); setTags(nextTags); } // Cache mit SHOW-Resultaten füllen React.useEffect(() => { const fromResults: Record = {}; for (const r of results) { if (r.type === "SHOW") { const uiShow = mapApiShowToUI(r.data); if (uiShow?.showId != null) fromResults[Number(uiShow.showId)] = uiShow as Show; } } if (Object.keys(fromResults).length) { setShowsById((prev) => ({ ...prev, ...fromResults })); } }, [results]); // SEASON-Ergebnisse nach showId gruppieren const seasonsByShowId = React.useMemo(() => { const map = new Map(); for (const r of results) { if (r.type !== "SEASON") continue; const s = mapApiSeasonToUI(r.data); if (!s || s.showId == null) continue; const key = Number(s.showId); const list = map.get(key) ?? []; list.push(s as Season); map.set(key, list); } for (const [k, list] of map) { list.sort((a, b) => { const da = a?.startDate ? new Date(a.startDate).getTime() : Number.POSITIVE_INFINITY; const db = b?.startDate ? new Date(b.startDate).getTime() : Number.POSITIVE_INFINITY; return da - db; }); map.set(k, list); } return map; }, [results]); // Fehlende Shows für Carousels nachladen React.useEffect(() => { const needed = Array.from(seasonsByShowId.keys()).filter((id) => !showsById[id]); if (needed.length === 0) return; let cancelled = false; (async () => { try { const fetched = await Promise.all(needed.map((id) => getShowById(id))); if (cancelled) return; const next: Record = {}; for (const s of fetched) { if (!s) continue; next[s.id] = s; } if (Object.keys(next).length) { setShowsById((prev) => ({ ...prev, ...next })); } } catch (e) { console.error(e); } })(); return () => { cancelled = true; }; }, [seasonsByShowId, showsById]); // PERSON-Resultate const persons = React.useMemo(() => { return results .filter((r) => r.type === "PERSON") .map((r) => mapApiPersonToUI(r.data)) .sort((a, b) => (a.name || "").localeCompare(b.name || "")); }, [results]); // SHOW-Resultate, die KEINE Staffeln haben -> als einzelne ShowBox anzeigen const standaloneShows: Show[] = React.useMemo(() => { const seen = new Set(); const list: Show[] = []; for (const r of results) { if (r.type !== "SHOW") continue; const ui = mapApiShowToUI(r.data) as Show | undefined; if (!ui?.id) continue; const id = Number(ui.id); if (seasonsByShowId.has(id)) continue; // bereits als Carousel vorhanden -> nicht doppelt if (seen.has(id)) continue; seen.add(id); list.push(ui); } return list; }, [results, seasonsByShowId]); // Moderner Fehlerblock if (error) { return ( ⚠️ Fehler beim Laden {error?.message || "Ein unerwarteter Fehler ist aufgetreten."} { if (typeof refetch === "function") refetch(); }} style={{ marginTop: 6, backgroundColor: "rgba(255,255,255,0.15)", paddingVertical: 10, paddingHorizontal: 18, borderRadius: 8, }} > Erneut versuchen ); } const noResults = persons.length === 0 && seasonsByShowId.size === 0 && standaloneShows.length === 0; return ( Durchsuchen { Keyboard.dismiss(); setShowSuggestions(false); }} > { setQuery(t); if (t.length > 0 && !showSuggestions) setShowSuggestions(true); if (t.length === 0) setShowSuggestions(false); }} placeholder="Wonach suchst du?" placeholderTextColor="" style={styles.searchInput} returnKeyType="search" onFocus={() => setShowSuggestions(true)} onSubmitEditing={() => { if (!query.trim()) return; tagAdded({ type: "CUSTOM", text: query.trim() }); }} autoCapitalize="none" /> {query.length === 0 ? ( ) : ( { setQuery(""); setShowSuggestions(false); }} /> )} {tags.map((tag) => ( { tagRemoved(tag); }} /> ))} {/* Suggestions dropdown */} {query.length > 0 && showSuggestions && ( Suchvorschläge {suggestions.map((suggestion, idx) => ( tagAdded(suggestion)} > {suggestion.text} ))} )} {/* Results */} {/* Personen Section (top) */} {persons.length > 0 && ( Personen {persons.slice(0, 5).map((p) => ( ))} )} {(seasonsByShowId.size > 0 || standaloneShows.length > 0) && ( Staffeln & Shows )} {/* Carousels für Shows mit Staffeln */} {Array.from(seasonsByShowId.entries()).map(([showId, seasons]) => { const show = showsById[Number(showId)]; if (!seasons || seasons.length === 0) return null; if (!show) { return ( ( )} /> ); } return ( ( )} /> ); })} {/* Einzelne Shows (ohne Staffeln) */} {standaloneShows.map((show) => ( ))} {/* Schöner Empty-State */} {noResults && ( Keine Ergebnisse gefunden Passen Sie Ihre Tags an oder setzen Sie die Filter zurück. { setTags([]); setQuery(""); setShowSuggestions(false); Keyboard.dismiss(); if (typeof refetch === "function") refetch(); }} style={{ backgroundColor: "rgba(255,255,255,0.15)", paddingVertical: 10, paddingHorizontal: 14, borderRadius: 8, }} > Filter zurücksetzen { setQuery(""); setShowSuggestions(false); Keyboard.dismiss(); }} style={{ backgroundColor: "rgba(255,255,255,0.08)", paddingVertical: 10, paddingHorizontal: 14, borderRadius: 8, }} > Eingabe löschen )} ); }