From a01ffcd2bb293306abf4feb3041814089af47b9c Mon Sep 17 00:00:00 2001 From: Cron1cle <118773725+Cron1cle@users.noreply.github.com> Date: Wed, 8 Oct 2025 15:02:31 +0200 Subject: [PATCH] :api added explore auto-complete feature --- apis/autoCompleteApi.ts | 24 +++++++ app/(tabs)/explore.tsx | 115 ++++++++++++++++++++++++++++++++-- app/(tabs)/index.tsx | 1 + app/_layout.tsx | 35 ++++++----- app/tabStyles/indexStyles.tsx | 100 ++++++++++++++++++++++++++++- contexts/DiscoveryContext.tsx | 96 ++++++++++++++++++++++++++++ 6 files changed, 350 insertions(+), 21 deletions(-) create mode 100644 apis/autoCompleteApi.ts create mode 100644 contexts/DiscoveryContext.tsx diff --git a/apis/autoCompleteApi.ts b/apis/autoCompleteApi.ts new file mode 100644 index 0000000..e9fb591 --- /dev/null +++ b/apis/autoCompleteApi.ts @@ -0,0 +1,24 @@ +export type AutoCompleteItemType = "SHOW" | "PERSON" | "SEASON" | string; + +export type AutoCompleteItem = { + type: AutoCompleteItemType; + text: string; +}; + +const DISCOVER_BASE = "http://45.157.177.99:8080/discover"; + +export async function getAutoComplete( + query: string, + limit = 10, + signal?: AbortSignal +): Promise { + if (!query.trim()) return []; + const url = `${DISCOVER_BASE}/autoComplete?q=${encodeURIComponent( + query + )}&limit=${limit}`; + const res = await fetch(url, { signal }); + if (!res.ok) throw new Error("AutoComplete failed " + res.status); + const data: unknown = await res.json(); + if (!Array.isArray(data)) return []; + return data as AutoCompleteItem[]; +} diff --git a/app/(tabs)/explore.tsx b/app/(tabs)/explore.tsx index 3508a11..6a6e593 100644 --- a/app/(tabs)/explore.tsx +++ b/app/(tabs)/explore.tsx @@ -1,11 +1,118 @@ import styles from "@/app/tabStyles/indexStyles"; import React from "react"; -import { Text, View } from "react-native"; +import { + Text, + View, + TextInput, + FlatList, + TouchableOpacity, + Keyboard, + TouchableWithoutFeedback, +} from "react-native"; +import Feather from "@expo/vector-icons/Feather"; +import { useDiscoveryContext } from "@/contexts/DiscoveryContext"; +import { useShowContext } from "@/contexts/ShowContext"; export default function TabTwoScreen() { + const { query, setQuery, suggestions, loading, error, clear } = + useDiscoveryContext(); + + const { shows } = useShowContext(); + + const personSuggestions = React.useMemo( + () => suggestions.filter((s) => s.type === "PERSON"), + [suggestions] + ); + + const showSuggestions = React.useMemo( + () => suggestions.filter((s) => s.type === "SHOW"), + [suggestions] + ); + + const [tag, setTag] = React.useState(null); return ( - - Explore Screen - + + + + Durchsuchen + + + + { + console.log("Search:", query); + }} + autoCapitalize="none" + /> + + {query.length === 0 ? ( + + ) : ( + setQuery("")} + /> + )} + + + {tag && ( + + {tag} + setTag(null)} + style={{ marginLeft: "auto", marginRight: 10 }} + /> + + )} + + {query.length > 0 && ( + <> + + Suchvorschläge + + {showSuggestions.map((suggestion, idx) => ( + { + setTag(suggestion.text); + }} + > + + {suggestion.text} + + ))} + {personSuggestions.map((suggestion, idx) => ( + { + setTag(suggestion.text); + }} + > + + {suggestion.text} + + ))} + + + )} + + ); } diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 26c4bd6..60db3a9 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -87,6 +87,7 @@ export default function HomeScreen() { alignItems: "center", paddingHorizontal: 10, gap: 10, + marginLeft: 10, }} > {uniqueStreamingServices.map((serviceName) => { diff --git a/app/_layout.tsx b/app/_layout.tsx index 23311f2..b0138a9 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -2,6 +2,7 @@ import { ShowProvider } from "@/contexts/ShowContext"; import { SeasonProvider } from "@/contexts/SeasonContext"; import { StreamingServiceProvider } from "@/contexts/StreamingServiceContext"; import { PersonProvider } from "@/contexts/PersonContext"; +import { DiscoveryProvider } from "@/contexts/DiscoveryContext"; import { Stack } from "expo-router"; import "react-native-reanimated"; @@ -11,22 +12,24 @@ export default function RootLayout() { - - - - - + + + + + + + diff --git a/app/tabStyles/indexStyles.tsx b/app/tabStyles/indexStyles.tsx index 561002b..94752da 100644 --- a/app/tabStyles/indexStyles.tsx +++ b/app/tabStyles/indexStyles.tsx @@ -40,7 +40,105 @@ export default StyleSheet.create({ filterSection: { width: "100%", height: 70, - backgroundColor: "hsl(221, 39%, 5%)", marginTop: 20, }, + searchContainer: { + width: "90%", + height: 60, + marginHorizontal: "auto", + backgroundColor: "hsl(221, 39%, 8%)", + 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, + }, + shadowOpacity: 0.25, + shadowRadius: 3.84, + elevation: 5, + }, + searchLabel: { + color: "hsl(221, 39%, 80%)", + fontSize: 18, + fontWeight: "500", + }, + suggestionsSection: { + width: "90%", + height: "auto", + paddingBottom: 15, + borderRadius: 20, + backgroundColor: "hsl(221, 39%, 8%)", + borderWidth: 1.5, + borderColor: "hsl(221, 39%, 15%)", + marginHorizontal: "auto", + alignSelf: "center", + marginTop: 15, + shadowColor: "#000", + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.25, + shadowRadius: 3.84, + elevation: 5, + }, + suggestionTitle: { + color: "hsl(0, 0%, 60%)", + fontSize: 14, + marginLeft: 15, + marginTop: 15, + fontWeight: "500", + }, + suggestionContainer: { + marginTop: 15, + width: "100%", + height: 30, + flexDirection: "row", + alignItems: "center", + justifyContent: "flex-start", + paddingHorizontal: 10, + }, + imageContainer: { + width: 20, + height: 20, + borderRadius: 10, + borderWidth: 1.5, + borderColor: "hsl(0, 0%, 90%)", + }, + suggestionLabel: { + color: "white", + fontSize: 12, + fontWeight: "500", + marginLeft: 10, + }, + tagContainer: { + flexDirection: "row", + alignItems: "center", + backgroundColor: "hsl(221, 39%, 8%)", + borderWidth: 1.5, + borderColor: "hsl(221, 39%, 15%)", + height: "auto", + width: "auto", + paddingVertical: 10, + borderRadius: 50, + marginTop: 15, + marginLeft: 20, + + justifyContent: "space-between", + }, + tagLabel: { + color: "hsl(0, 0%, 90%)", + fontSize: 14, + fontWeight: "500", + marginLeft: 15, + textAlign: "center", + }, }); diff --git a/contexts/DiscoveryContext.tsx b/contexts/DiscoveryContext.tsx new file mode 100644 index 0000000..ff94906 --- /dev/null +++ b/contexts/DiscoveryContext.tsx @@ -0,0 +1,96 @@ +import React, { + createContext, + useContext, + useEffect, + useRef, + useState, + useCallback, +} from "react"; +import { getAutoComplete, AutoCompleteItem } from "@/apis/autoCompleteApi"; + +type DiscoveryContextType = { + query: string; + setQuery: (q: string) => void; + suggestions: AutoCompleteItem[]; + loading: boolean; + error: string | null; + clear: () => void; +}; + +const DiscoveryContext = createContext(null); + +export const DiscoveryProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const [query, setQuery] = useState(""); + const [suggestions, setSuggestions] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const abortRef = useRef(null); + const debounceRef = useRef | null>(null); + const cacheRef = useRef>({}); + + 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 clear = () => { + setQuery(""); + setSuggestions([]); + setError(null); + }; + + return ( + + {children} + + ); +}; + +export const useDiscoveryContext = () => { + const ctx = useContext(DiscoveryContext); + if (!ctx) + throw new Error( + "useDiscoveryContext must be used within DiscoveryProvider" + ); + return ctx; +};