import { AutoCompleteItem } from "@/apis/autoCompleteApi"; import { Season } from "@/apis/seasonApi"; import { Show } from "@/apis/showApi"; import { Colors } from "@/constants/colors"; import { useDiscoveryContext } from "@/contexts/DiscoveryContext"; import Feather from "@expo/vector-icons/Feather"; import { Stack } from "expo-router"; import React from "react"; import { Dimensions, Image, ScrollView, StyleSheet, Text, TouchableOpacity, View, } from "react-native"; import { getShowById } from "@/apis/showApi"; import PersonRow from "@/components/discovery/PersonRow"; import TagChip from "@/components/discovery/TagChip"; import { useSearch } from "@/hooks/useSearch"; import { getIconName, mapApiPersonToUI, mapApiSeasonToUI, mapApiShowToUI, } from "@/utils/searchMapping"; import { router } from "expo-router"; 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); } 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: alle Shows (direkte Treffer + aus Seasons) vereint const unifiedShows = React.useMemo(() => { const map = new Map(); // Shows aus Season-Ergebnissen for (const [showId, seasons] of seasonsByShowId) { const show = showsById[Number(showId)]; if (show) { map.set(Number(showId), { show, seasons }); } } // Direkte Show-Treffer (ohne Seasons) for (const r of results) { if (r.type !== "SHOW") continue; const uiShow = mapApiShowToUI(r.data); const id = Number(uiShow.id); if (!map.has(id)) { const cachedShow = showsById[id]; map.set(id, { show: cachedShow ?? uiShow, seasons: [] }); } } return Array.from(map.values()); }, [results, seasonsByShowId, showsById]); // Fehlerblock if (error) { return ( Fehler beim Laden {error?.message || "Ein unerwarteter Fehler ist aufgetreten."} { if (typeof refetch === "function") refetch(); }} style={s.emptyButton} activeOpacity={0.7} > Erneut versuchen ); } const noResults = persons.length === 0 && unifiedShows.length === 0; return ( { const text = e.nativeEvent.text; setQuery(text); setShowSuggestions(text.length > 0); }, onSearchButtonPress: (e) => { const text = e.nativeEvent.text; if (!text?.trim()) return; tagAdded({ type: "CUSTOM", text: text.trim() }); }, onCancelButtonPress: () => { setQuery(""); setShowSuggestions(false); }, }, }} /> {/* Suggestions overlay */} {query.length > 0 && showSuggestions && suggestions.length > 0 && ( VORSCHLÄGE {suggestions.slice(0, 10).map((suggestion, idx, arr) => ( tagAdded(suggestion)} activeOpacity={0.6} > {suggestion.text} ))} )} {/* Tag chips */} {tags.length > 0 && ( {tags.map((tag) => ( tagRemoved(tag)} /> ))} )} {/* Personen Section */} {persons.length > 0 && ( PERSONEN {persons.slice(0, 5).map((p, i, arr) => ( ))} )} {/* Shows + Staffeln unified */} {unifiedShows.length > 0 && ( SHOWS {unifiedShows.map(({ show, seasons }) => ( router.push({ pathname: "/showDetails", params: { id: String(show.id) }, }) } > {show.title} {show.description ? ( {show.description} ) : null} {show.running && ( LIVE )} {seasons.length > 0 && ( {seasons.map((season) => ( S{(season as any).seasonNumber} {(season as any).startDate ? ` · ${new Date((season as any).startDate).getFullYear()}` : ""} ))} )} ))} )} {/* Empty State */} {noResults && ( Keine Ergebnisse Passe deine Tags an oder setze die Filter zurück. {tags.length > 0 && ( { setTags([]); setQuery(""); setShowSuggestions(false); if (typeof refetch === "function") refetch(); }} style={s.emptyButton} activeOpacity={0.7} > Filter zurücksetzen )} )} ); } const s = StyleSheet.create({ container: { flex: 1, backgroundColor: Colors.background, }, section: { paddingHorizontal: 16, marginBottom: 20, paddingTop: 20, }, sectionHeader: { color: "rgba(255,255,255,0.45)", fontSize: 13, fontWeight: "600", letterSpacing: 0.5, marginBottom: 8, paddingLeft: 4, }, groupedCard: { borderRadius: 10, overflow: "hidden", }, tagRow: { flexDirection: "row", flexWrap: "wrap", }, // Suggestions overlay suggestionsOverlay: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, zIndex: 10, paddingHorizontal: 16, paddingTop: 6, backgroundColor: Colors.background, }, suggestionsHeader: { color: "rgba(255,255,255,0.45)", fontSize: 12, fontWeight: "600", letterSpacing: 0.5, marginBottom: 6, paddingLeft: 4, marginTop: Dimensions.get("screen").height / 7, }, suggestionsGroup: { borderRadius: 10, overflow: "hidden", }, suggestionRow: { flexDirection: "row", alignItems: "center", backgroundColor: "rgba(255,255,255,0.06)", paddingHorizontal: 14, paddingVertical: 10, gap: 10, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: "rgba(255,255,255,0.08)", }, suggestionText: { color: "white", fontSize: 15, flex: 1, }, // Empty state emptyState: { alignItems: "center", justifyContent: "center", paddingVertical: 48, paddingHorizontal: 32, gap: 8, }, emptyTitle: { fontSize: 20, fontWeight: "600", color: "rgba(255,255,255,0.8)", marginTop: 8, }, emptySubtitle: { fontSize: 15, color: "rgba(255,255,255,0.45)", textAlign: "center", lineHeight: 20, }, emptyButton: { marginTop: 16, backgroundColor: "rgba(255,255,255,0.1)", paddingVertical: 10, paddingHorizontal: 20, borderRadius: 10, }, emptyButtonText: { color: Colors.primary, fontSize: 15, fontWeight: "600", }, // Unified show cards showCard: { borderRadius: 16, overflow: "hidden", backgroundColor: "rgba(255,255,255,0.06)", borderWidth: StyleSheet.hairlineWidth, borderColor: "rgba(255,255,255,0.08)", marginBottom: 12, }, showCardImage: { width: "100%", height: 160, }, showCardBody: { padding: 14, gap: 10, }, showCardHeader: { flexDirection: "row", alignItems: "flex-start", gap: 10, }, showCardTitle: { color: "#fff", fontSize: 17, fontWeight: "700", letterSpacing: 0.1, }, showCardDescription: { color: "rgba(255,255,255,0.45)", fontSize: 13, lineHeight: 18, marginTop: 4, }, liveBadge: { backgroundColor: "rgba(255,59,48,0.2)", borderWidth: StyleSheet.hairlineWidth, borderColor: "rgba(255,59,48,0.4)", paddingHorizontal: 8, paddingVertical: 3, borderRadius: 10, }, liveBadgeText: { color: "#ff3b30", fontSize: 10, fontWeight: "700", letterSpacing: 0.5, }, seasonRow: { flexDirection: "row", flexWrap: "wrap", gap: 6, }, seasonPill: { paddingVertical: 5, paddingHorizontal: 12, borderRadius: 14, backgroundColor: "rgba(25,158,219,0.15)", borderWidth: StyleSheet.hairlineWidth, borderColor: "rgba(25,158,219,0.25)", }, seasonPillText: { color: "#199edb", fontSize: 12, fontWeight: "600", }, });