Files
fltr-app/app/(tabs)/explore.tsx
2025-10-29 20:50:21 +11:00

238 lines
8.3 KiB
TypeScript

import { AutoCompleteItem } from "@/apis/autoCompleteApi";
import { SearchResultItem } from "@/apis/searchApi";
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, 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";
import TagChip from "@/components/discovery/TagChip";
import { getIconName, mapApiPersonToUI, mapApiSeasonToUI, mapApiShowToUI } from "@/utils/searchMapping";
export default function ExploreScreen() {
const { query, setQuery, suggestions } = useDiscoveryContext();
const [tags, setTags] = React.useState<AutoCompleteItem[]>([]);
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 ---
function tagAdded(tag: AutoCompleteItem) {
const nextTags = tags.some((t) => t.text === tag.text) ? tags : [...tags, tag];
setTags(nextTags);
setQuery("");
Keyboard.dismiss();
}
function tagRemoved(tag: AutoCompleteItem) {
const nextTags = tags.filter((t) => t.text !== tag.text);
setTags(nextTags);
}
// Keep our local show cache in sync with SHOW items returned by search
React.useEffect(() => {
const fromResults: Record<number, Show> = {};
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]);
// Group SEASON results by showId
const seasonsByShowId = React.useMemo(() => {
const map = new Map<number, Season[]>();
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);
}
// sort seasons per show by startDate asc
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]);
// Lazy fetch missing shows needed for Season carousels
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<number, Show> = {};
for (const s of fetched) {
if (!s) continue;
next[s.id] = s; // wichtig: s.id, nicht s.showId
}
if (Object.keys(next).length) {
setShowsById((prev) => ({ ...prev, ...next }));
}
} catch (e) {
console.error(e);
}
})();
return () => { cancelled = true; };
}, [seasonsByShowId, showsById]);
// PERSON hits shown at the top (like old screen)
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]);
return (
<View style={[styles.mainContainer]}>
<View style={styles.header}>
<Text style={[styles.title, { fontSize: 28 }]}>Durchsuchen</Text>
</View>
<View style={styles.sectionContainer}>
{/* Search bar */}
<View style={styles.searchContainer}>
<TextInput
value={query}
onChangeText={setQuery}
placeholder="Wonach suchst du?"
placeholderTextColor=""
style={styles.searchInput}
returnKeyType="search"
onSubmitEditing={() => {
if (!query.trim()) return;
tagAdded({ type: "CUSTOM", text: query.trim() });
}}
autoCapitalize="none"
/>
{query.length === 0 ? (
<Feather name="search" size={24} color="hsl(221, 39%, 80%)" />
) : (
<Feather name="x" size={24} color="hsl(221, 39%, 80%)" onPress={() => setQuery("")} />
)}
</View>
{/* Tag chips */}
<View style={styles.tagContainer}>
{tags.map((tag) => (
<TagChip
key={tag.text}
icon={getIconName(tag.type)}
label={tag.text}
onPress={() => {
tagRemoved(tag);
}}
/>
))}
</View>
{/* Suggestions dropdown */}
{query.length > 0 && (
<View style={styles.suggestionsSection}>
<Text style={styles.suggestionTitle}>Suchvorschläge</Text>
<ScrollView keyboardShouldPersistTaps="handled">
{suggestions.map((suggestion, idx) => (
<TouchableOpacity
key={suggestion.text + "_" + idx}
style={styles.suggestionContainer}
onPress={() => tagAdded(suggestion)}
>
<FontAwesome name={getIconName(suggestion.type)} size={16} color="hsl(0, 0%, 90%)" />
<Text style={styles.suggestionLabel}>{suggestion.text}</Text>
</TouchableOpacity>
))}
</ScrollView>
</View>
)}
</View>
{/* Results */}
<View style={{ flex: 1 }}>
<ScrollView keyboardShouldPersistTaps="handled">
{/* Personen Section (top) */}
{persons.length > 0 && (
<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}/>
))}
</View>
)}
{/* Staffeln grouped by Show with page view */}
<View style={styles.sectionContainer}>
<Text style={styles.sectionTitle}>Staffeln</Text>
{Array.from(seasonsByShowId.entries()).map(([showId, seasons]) => {
const show = showsById[Number(showId)];
if (!seasons || seasons.length === 0) return null;
// If show metadata is not yet loaded, render a minimal ShowBox fallback once per page item
if (!show) {
return (
<SeasonCarousel
key={`sc-${showId}`}
show={{ showId: showId as any, title: "blaaa", description: "", genre: "", thumbnailUrl: "", running: false } as any}
seasons={seasons}
renderItem={(s) => <ShowBox show={{ showId: showId as any, title: `Show #${showId}`, description: "", genre: "", thumbnailUrl: "", running: false } as any} displayedSeason={s} shadow={false} />}
/>
);
}
return (
<SeasonCarousel
key={`sc-${showId}`}
show={show}
seasons={seasons}
renderItem={(s) => <ShowBox show={show} displayedSeason={s} shadow={false} />}
/>
);
})}
{seasonsByShowId.size === 0 && (
<Text style={styles.centerText}>
Keine Staffeln gefunden. Passe deine Tags an.
</Text>
)}
</View>
</ScrollView>
</View>
</View>
);
}