diff --git a/apis/searchApi.ts b/apis/searchApi.ts index 5d2b91d..099dd69 100644 --- a/apis/searchApi.ts +++ b/apis/searchApi.ts @@ -7,22 +7,27 @@ export type SearchResultItem = { const DISCOVER_BASE = "https://fltr-app.de/api/discover/search"; -export async function getSearchResults( - tags: string[] | string, - limit = 10, - signal?: AbortSignal +export async function discoverSearch( + tags: string[], + signal?: AbortSignal, ): Promise { - const tagList = Array.isArray(tags) ? tags : [tags]; - const filteredTags = tagList.map((t) => t.trim()).filter(Boolean); + const filteredTags = tags.map((t) => t.trim()).filter(Boolean); if (!filteredTags.length) return []; - const url = `${DISCOVER_BASE}?tags=${encodeURIComponent( - filteredTags.join(",") - )}&limit=${limit}`; + const params = filteredTags + .map((tag) => `tags=${encodeURIComponent(tag)}`) + .join("&"); + const url = `${DISCOVER_BASE}?${params}`; const apiKey = process.env.EXPO_PUBLIC_API_KEY; - const res = await fetch(url, { signal, headers: { 'Content-Type': 'application/json', "X-API-Key": apiKey ?? "", } }); - if (!res.ok) throw new Error("AutoComplete failed " + res.status); + const res = await fetch(url, { + signal, + headers: { + "Content-Type": "application/json", + "X-API-Key": apiKey ?? "", + }, + }); + if (!res.ok) throw new Error("Discover search failed " + res.status); const data: unknown = await res.json(); if (!Array.isArray(data)) return []; diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 949b049..23bf68f 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -1,40 +1,45 @@ import Feather from "@expo/vector-icons/Feather"; -import { Tabs } from "expo-router"; -import React from "react"; +import * as Haptics from "expo-haptics"; +import { useNavigation } from "expo-router"; +import { + Icon, + Label, + NativeTabs, + VectorIcon, +} from "expo-router/unstable-native-tabs"; +import React, { useEffect, useRef } from "react"; export default function TabLayout() { + const navigation = useNavigation(); + const isInitial = useRef(true); + + useEffect(() => { + const unsubscribe = navigation.addListener("state", () => { + if (isInitial.current) { + isInitial.current = false; + return; + } + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + }); + return unsubscribe; + }, [navigation]); + return ( - - ( - - ), - }} - /> - ( - - ), - }} - /> - + + + + } + /> + + + + } + /> + + ); } diff --git a/app/(tabs)/explore.tsx b/app/(tabs)/explore.tsx deleted file mode 100644 index 2d5b0d8..0000000 --- a/app/(tabs)/explore.tsx +++ /dev/null @@ -1,443 +0,0 @@ -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, - // isLoading, // optional, falls benötigt - } = 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]); - - // 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; - - 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 && ( - Staffeln - )} - - {Array.from(seasonsByShowId.entries()).map(([showId, seasons]) => { - const show = showsById[Number(showId)]; - if (!seasons || seasons.length === 0) return null; - - if (!show) { - return ( - ( - - )} - /> - ); - } - return ( - ( - - )} - /> - ); - })} - - {/* 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 - - - - )} - - - - - - - ); -} diff --git a/app/(tabs)/explore/_layout.tsx b/app/(tabs)/explore/_layout.tsx new file mode 100644 index 0000000..2152f89 --- /dev/null +++ b/app/(tabs)/explore/_layout.tsx @@ -0,0 +1,34 @@ +import { Colors } from "@/constants/colors"; +import { Stack } from "expo-router"; + +export default function ExploreLayout() { + return ( + + + + ); +} diff --git a/app/(tabs)/explore/index.tsx b/app/(tabs)/explore/index.tsx new file mode 100644 index 0000000..ddc6e24 --- /dev/null +++ b/app/(tabs)/explore/index.tsx @@ -0,0 +1,579 @@ +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", + }, +}); diff --git a/app/(tabs)/home/_layout.tsx b/app/(tabs)/home/_layout.tsx new file mode 100644 index 0000000..6b76cc7 --- /dev/null +++ b/app/(tabs)/home/_layout.tsx @@ -0,0 +1,30 @@ +import { Colors } from "@/constants/colors"; +import { Stack } from "expo-router"; + +export default function HomeLayout() { + return ( + + + + ); +} diff --git a/app/(tabs)/home/index.tsx b/app/(tabs)/home/index.tsx new file mode 100644 index 0000000..dd7a5c2 --- /dev/null +++ b/app/(tabs)/home/index.tsx @@ -0,0 +1,353 @@ +import ShowCard from "@/components/ui/ShowCard"; +import { Colors } from "@/constants/colors"; +import { useShows } from "@/hooks/useShows"; +import { useStreamingServices } from "@/hooks/useStreamingServices"; +import Feather from "@expo/vector-icons/Feather"; +import * as Haptics from "expo-haptics"; +import { router, Stack } from "expo-router"; +import React from "react"; +import { + ActivityIndicator, + Image, + Platform, + RefreshControl, + ScrollView, + StyleSheet, + Text, + TouchableOpacity, + View, +} from "react-native"; + +export default function HomeScreen() { + const { + data: shows = [], + error, + isLoading: loading, + refetch: refetchShows, + } = useShows(); + + const { data: streamingServices = {}, refetch: refetchServices } = + useStreamingServices(); + + const [activeFilter, setActiveFilter] = React.useState("all"); + const [refreshing, setRefreshing] = React.useState(false); + + const haptikFeedback = () => { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + }; + + const handleFilter = (type: string) => { + haptikFeedback(); + setActiveFilter(type === activeFilter ? "all" : type); + }; + + const onRefresh = React.useCallback(async () => { + haptikFeedback(); + setRefreshing(true); + try { + await Promise.all([ + typeof refetchShows === "function" ? refetchShows() : Promise.resolve(), + typeof refetchServices === "function" + ? refetchServices() + : Promise.resolve(), + ]); + } finally { + setRefreshing(false); + } + }, [refetchShows, refetchServices]); + + const filteredShows = React.useMemo(() => { + if (activeFilter === "all") return shows; + if (activeFilter === "live") return shows.filter((show) => show.running); + return shows.filter((show) => + show.streamingService + .split(",") + .map((s) => s.trim()) + .includes(activeFilter), + ); + }, [shows, activeFilter]); + + const uniqueStreamingServices = React.useMemo(() => { + const uniqueServices = new Set(); + shows.forEach((show) => { + show.streamingService + .split(", ") + .map((s) => s.trim()) + .forEach((service) => uniqueServices.add(service)); + }); + return Array.from(uniqueServices); + }, [shows]); + + if (loading) { + return ( + + + + ); + } + + if (error) { + return ( + + + ⚠️ + Fehler beim Laden + + {error?.message || "Ein unerwarteter Fehler ist aufgetreten."} + + { + if (typeof refetchShows === "function") refetchShows(); + if (typeof refetchServices === "function") refetchServices(); + }} + style={s.retryButton} + > + Erneut versuchen + + + + ); + } + + return ( + + ( + router.push("/legal")} hitSlop={8}> + + + ), + }} + /> + + + } + > + {/* Filter chips */} + + handleFilter("all")} + activeOpacity={0.7} + > + + Alle + + + + handleFilter("live")} + activeOpacity={0.7} + > + + + Live + + + + {uniqueStreamingServices.map((serviceName) => { + const serviceUri = + streamingServices[ + `assets.images.streamingServices.${serviceName.toLowerCase()}` + ]; + const isActive = activeFilter === serviceName; + return ( + handleFilter(serviceName)} + activeOpacity={0.7} + > + {serviceUri ? ( + + ) : ( + {serviceName} + )} + + ); + })} + + + {/* Show cards */} + + {filteredShows.map((show) => ( + + router.push({ + pathname: "/showDetails", + params: { + id: String(show.id), + title: show.title, + bannerUri: show.bannerUri, + description: show.description, + concept: show.concept, + genres: show.genres, + streamingService: show.streamingService, + logoUri: show.logoUrl, + running: String(show.running), + }, + }) + } + imageUri={show.bannerUri} + streamingServicesUris={show.streamingService + .split(", ") + .map( + (sv) => + streamingServices[ + `assets.images.streamingServices.${sv.toLowerCase()}` + ], + )} + genres={show.genres} + {...(show.running + ? { + liveBadgeText: "LIVE", + } + : {})} + /> + ))} + + + + ); +} + +const s = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: Colors.background, + }, + centered: { + flex: 1, + backgroundColor: Colors.background, + justifyContent: "center", + alignItems: "center", + padding: 20, + }, + errorCard: { + alignItems: "center", + gap: 10, + backgroundColor: "rgba(255,255,255,0.06)", + paddingVertical: 28, + paddingHorizontal: 24, + borderRadius: 16, + width: "90%", + }, + errorTitle: { + fontSize: 17, + fontWeight: "600", + color: "white", + textAlign: "center", + }, + errorMessage: { + fontSize: 14, + color: "rgba(255,255,255,0.55)", + textAlign: "center", + lineHeight: 20, + }, + retryButton: { + marginTop: 8, + backgroundColor: "rgba(255,255,255,0.12)", + paddingVertical: 10, + paddingHorizontal: 20, + borderRadius: 10, + }, + retryText: { + color: "white", + fontWeight: "600", + fontSize: 15, + }, + + /* Filter row */ + filterRow: { + paddingHorizontal: 16, + paddingTop: 8, + paddingBottom: 4, + gap: 8, + alignItems: "center", + }, + filterPill: { + flexDirection: "row", + alignItems: "center", + gap: 5, + paddingHorizontal: 16, + paddingVertical: 8, + borderRadius: 20, + backgroundColor: "rgba(255,255,255,0.08)", + }, + filterPillActive: { + backgroundColor: "rgba(255,255,255,0.22)", + }, + filterPillText: { + color: "rgba(255,255,255,0.7)", + fontSize: 14, + fontWeight: "600", + }, + filterPillTextActive: { + color: "white", + }, + liveDot: { + width: 7, + height: 7, + borderRadius: 4, + backgroundColor: "#ff3b30", + }, + serviceChip: { + width: 44, + height: 44, + borderRadius: 22, + backgroundColor: "rgba(255,255,255,0.08)", + alignItems: "center", + justifyContent: "center", + overflow: "hidden", + }, + serviceChipActive: { + borderWidth: 2, + borderColor: Colors.primary, + }, + serviceIcon: { + width: 40, + height: 40, + borderRadius: 20, + resizeMode: "contain", + }, + + /* Card list */ + cardList: { + paddingHorizontal: 16, + paddingBottom: 30, + }, +}); diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 19eca20..b41713f 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -1,356 +1,5 @@ -import styles from "@/app/tabStyles/indexStyles"; -import ShowCard from "@/components/ui/ShowCard"; -import { useShows } from "@/hooks/useShows"; -import { useStreamingServices } from "@/hooks/useStreamingServices"; -import Feather from "@expo/vector-icons/Feather"; -import * as Haptics from "expo-haptics"; -import { router } from "expo-router"; -import React from "react"; -import { - ActivityIndicator, - Image, - RefreshControl, - ScrollView as RNScrollView, - Text, - TouchableOpacity, - View, -} from "react-native"; -import { - GestureHandlerRootView, - ScrollView, // horizontaler ScrollView bleibt aus RNGH -} from "react-native-gesture-handler"; +import { Redirect } from "expo-router"; -export default function HomeScreen() { - const { - data: shows = [], - error, - isLoading: loading, - refetch: refetchShows, // ⬅️ refetch aus Hook - } = useShows(); - - const { - data: streamingServices = {}, - refetch: refetchServices, // ⬅️ refetch aus Hook - } = useStreamingServices(); - - const [activeFilter, setActiveFilter] = React.useState("all"); - const [refreshing, setRefreshing] = React.useState(false); // ⬅️ UI-State für Pull-to-Refresh - - const haptikFeedback = () => { - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); - }; - - const handleFilter = (type: string) => { - haptikFeedback(); - if (type === activeFilter) { - setActiveFilter("all"); - } else { - setActiveFilter(type); - } - }; - - // ⬅️ Pull-to-Refresh Handler - const onRefresh = React.useCallback(async () => { - haptikFeedback(); - setRefreshing(true); - try { - await Promise.all([ - typeof refetchShows === "function" ? refetchShows() : Promise.resolve(), - typeof refetchServices === "function" - ? refetchServices() - : Promise.resolve(), - ]); - } finally { - setRefreshing(false); - } - }, [refetchShows, refetchServices]); - - const filteredShows = React.useMemo(() => { - if (activeFilter === "all") { - return shows; - } - if (activeFilter === "live") { - return shows.filter((show) => show.running); - } - return shows.filter((show) => - show.streamingService - .split(",") - .map((s) => s.trim()) - .includes(activeFilter) - ); - }, [shows, activeFilter]); - - const uniqueStreamingServices = React.useMemo(() => { - const uniqueServices = new Set(); - shows.forEach((show) => { - const services = show.streamingService.split(", ").map((s) => s.trim()); - services.forEach((service) => uniqueServices.add(service)); - }); - return Array.from(uniqueServices); - }, [shows]); - - if (loading) { - return ( - - - - ); - } - - if (error) { - return ( - - - ⚠️ - - Fehler beim Laden - - - {error?.message || "Ein unerwarteter Fehler ist aufgetreten."} - - - { - if (typeof refetchShows === "function") refetchShows(); - if (typeof refetchServices === "function") refetchServices(); - }} - style={{ - marginTop: 6, - backgroundColor: "rgba(255,255,255,0.15)", - paddingVertical: 10, - paddingHorizontal: 18, - borderRadius: 8, - }} - > - - Erneut versuchen - - - - - ); - } - - return ( - - - - { - router.push("/legal"); - }} - style={{ - position: "absolute", - left: 16, - top: "63%", - transform: [{ translateY: -12 }], - height: 40, - width: 40, - alignItems: "center", - justifyContent: "center", - borderRadius: 10, - backgroundColor: "rgba(255,255,255,0.06)", - }} - accessibilityRole="button" - accessibilityLabel="Menü öffnen" - > - - - - FLTR - - - - } - > - - {/* ⬅️ HORIZONTALER SCROLLBEREICH BLEIBT AUS RNGH */} - - {activeFilter !== "all" && ( - handleFilter("all")} - > - - ALLE - - - )} - {activeFilter !== "live" && ( - handleFilter("live")} - > - - - LIVE - - - - )} - - - - {uniqueStreamingServices.map((serviceName) => { - const streamingService = - streamingServices[ - `assets.images.streamingServices.${serviceName.toLowerCase()}` - ]; - return ( - handleFilter(serviceName)} - > - - - ); - })} - - - - - {filteredShows.map((show) => { - const showLiveBadge = show.running; - return ( - - router.push({ - pathname: "/showDetails", - params: { - id: String(show.id), - title: show.title, - bannerUri: show.bannerUri, - description: show.description, - concept: show.concept, - genres: show.genres, - streamingService: show.streamingService, - logoUri: show.logoUrl, - running: String(show.running), - }, - }) - } - imageUri={show.bannerUri} - streamingServicesUris={show.streamingService - .split(", ") - .map( - (s) => - streamingServices[ - `assets.images.streamingServices.${s.toLowerCase()}` - ] - )} - genres={show.genres} - {...(showLiveBadge - ? { - liveBadgeText: "LIVE", - liveBadgeContainerStyle: styles.liveBadgeContainer, - } - : {})} - /> - ); - })} - - - - - ); +export default function () { + return ; } diff --git a/app/_layout.tsx b/app/_layout.tsx index 7117d06..09b9fec 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,3 +1,4 @@ +import { Colors } from "@/constants/colors"; import { DiscoveryProvider } from "@/contexts/DiscoveryContext"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { Stack } from "expo-router"; @@ -9,28 +10,46 @@ export default function RootLayout() { return ( - - + + + headerShown: false, + }} + /> - ); diff --git a/app/legal.tsx b/app/legal.tsx index 9bdbae3..c5aa18c 100644 --- a/app/legal.tsx +++ b/app/legal.tsx @@ -1,154 +1,227 @@ +import { Colors } from "@/constants/colors"; import Feather from "@expo/vector-icons/Feather"; +import { BlurView } from "expo-blur"; import { router } from "expo-router"; import React from "react"; -import { ScrollView, Text, TouchableOpacity, View } from "react-native"; +import { + ScrollView, + StyleSheet, + Text, + TouchableOpacity, + View, +} from "react-native"; export default function LegalScreen() { return ( - - - - - - Info - - + + {/* Header */} + + Info router.back()} accessibilityRole="button" accessibilityLabel="Modal schließen" - style={{ - height: 40, - width: 40, - alignItems: "center", - justifyContent: "center", - borderRadius: 10, - backgroundColor: "rgba(255,255,255,0.08)", - }} + style={styles.closeButton} > - + - + - {/* Impressum Card */} - - - Impressum - - - - Berg Autosoft - Joe Felipe Berg - Stöckener Straße 35 - 30419 Hannover + {/* Impressum */} + + + + + + Impressum - - - - +49 1522 5642948 - kontakt@berg-autosoft.de + + Berg Autosoft + Joe Felipe Berg + Stöckener Straße 35 + 30419 Hannover - + - - Steuernummer: 25/103/17193 - USt-ID: DE361689728 + + + + +49 1522 5642948 + + + + kontakt@berg-autosoft.de + + + + + + + Steuernummer: 25/103/17193 + USt-ID: DE361689728 - {/* Support Card */} - - - Support - + {/* Support */} + + + + + + Support + - + Sollten Sie Probleme bei der Nutzung der iOS- oder Android-App FLTR haben, wenden Sie sich bitte direkt an den Support. - + - Schreiben Sie eine E-Mail an: - - developer@berg-autosoft.de - + Schreiben Sie eine E-Mail an: + + + developer@berg-autosoft.de + - + Wir bemühen uns, Ihr Anliegen so schnell wie möglich zu bearbeiten. {/* Footer */} - - - © 2025 Berg Autosoft - - + © 2025 Berg Autosoft ); } -const styles = { - mono: { +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: Colors.background, + }, + header: { + paddingTop: 18, + paddingHorizontal: 16, + paddingBottom: 14, + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: "rgba(255,255,255,0.08)", + overflow: "hidden", + }, + headerTitle: { + color: "white", + fontSize: 20, + fontWeight: "700", + letterSpacing: 0.3, + }, + closeButton: { + height: 32, + width: 32, + alignItems: "center", + justifyContent: "center", + borderRadius: 16, + backgroundColor: "rgba(255,255,255,0.1)", + borderWidth: StyleSheet.hairlineWidth, + borderColor: "rgba(255,255,255,0.12)", + }, + scrollContent: { + paddingHorizontal: 16, + paddingBottom: 40, + paddingTop: 12, + }, + card: { + backgroundColor: "rgba(255,255,255,0.06)", + borderRadius: 18, + borderWidth: StyleSheet.hairlineWidth, + borderColor: "rgba(255,255,255,0.08)", + padding: 18, + gap: 12, + marginTop: 10, + }, + cardHeader: { + flexDirection: "row", + alignItems: "center", + gap: 10, + marginBottom: 2, + }, + cardIconCircle: { + width: 32, + height: 32, + borderRadius: 16, + backgroundColor: "rgba(25,158,219,0.15)", + borderWidth: StyleSheet.hairlineWidth, + borderColor: "rgba(25,158,219,0.25)", + justifyContent: "center", + alignItems: "center", + }, + cardTitle: { + color: "white", + fontSize: 18, + fontWeight: "700", + }, + cardSection: { + gap: 4, + }, + divider: { + height: StyleSheet.hairlineWidth, + backgroundColor: "rgba(255,255,255,0.08)", + }, + infoRow: { + flexDirection: "row", + alignItems: "center", + gap: 10, + paddingVertical: 2, + }, + textPrimary: { color: "rgba(255,255,255,0.92)", - fontSize: 16, - } as const, - dim: { + fontSize: 15, + }, + textSecondary: { color: "rgba(255,255,255,0.75)", fontSize: 14, - } as const, - body: { - color: "rgba(255,255,255,0.88)", + }, + textDim: { + color: "rgba(255,255,255,0.5)", + fontSize: 13, + }, + textBody: { + color: "rgba(255,255,255,0.8)", fontSize: 14, lineHeight: 20, - } as const, -}; + }, + emailPill: { + flexDirection: "row", + alignItems: "center", + gap: 8, + paddingVertical: 10, + paddingHorizontal: 16, + borderRadius: 20, + backgroundColor: "rgba(25,158,219,0.12)", + borderWidth: StyleSheet.hairlineWidth, + borderColor: "rgba(25,158,219,0.25)", + alignSelf: "flex-start", + marginTop: 4, + }, + emailText: { + color: "#199edb", + fontSize: 14, + fontWeight: "600", + }, + footer: { + color: "rgba(255,255,255,0.35)", + fontSize: 12, + letterSpacing: 0.2, + textAlign: "center", + marginTop: 24, + }, +}); diff --git a/app/participant.tsx b/app/participant.tsx index e4f1dd2..9355a76 100644 --- a/app/participant.tsx +++ b/app/participant.tsx @@ -1,23 +1,32 @@ import { PersonMini } from "@/apis/personHistoryApi"; import styles from "@/app/stackStyles/participantStyles"; -import { usePersonHistory, AppearanceGroup } from "@/hooks/usePersonHistory"; +import { usePersonHistory } from "@/hooks/usePersonHistory"; import Ionicons from "@expo/vector-icons/Ionicons"; import { router, useLocalSearchParams } from "expo-router"; import * as WebBrowser from "expo-web-browser"; import React from "react"; -import { Image, Text, TouchableOpacity, View } from "react-native"; +import { + ActivityIndicator, + Image, + Text, + TouchableOpacity, + View, +} from "react-native"; import { GestureHandlerRootView, ScrollView, } from "react-native-gesture-handler"; export default function ParticipantScreen() { - const { name, participantId } = useLocalSearchParams(); + const { name, participantId, imageUri } = useLocalSearchParams(); const pid = Array.isArray(participantId) ? Number(participantId[0]) : Number(participantId); + const imageUriString = Array.isArray(imageUri) ? imageUri[0] : imageUri; + const isPravatar = imageUriString?.includes("pravatar"); + const { data: appearances = [], isLoading, isError } = usePersonHistory(pid); const formatYear = (iso?: string | null) => { @@ -27,7 +36,7 @@ export default function ParticipantScreen() { }; const [expandedShows, setExpandedShows] = React.useState>( - new Set() + new Set(), ); const toggleExpand = React.useCallback((showId: number) => { setExpandedShows((prev) => { @@ -45,193 +54,240 @@ export default function ParticipantScreen() { const goToPerson = React.useCallback( (p: PersonMini) => { if (!p?.personId) return; - if (p.personId === pid) return; router.push({ pathname: "/participant", - params: { participantId: String(p.personId), name: p.name }, + params: { + participantId: String(p.personId), + name: p.name, + imageUri: p.imageUrl || "", + }, }); }, - [pid] + [pid], ); return ( - {name} - router.back()} - > - - + {/* Profile Hero */} + + + + + {name} + + {appearances.length}{" "} + {appearances.length === 1 ? "Auftritt" : "Auftritte"} + + + + WebBrowser.openBrowserAsync( + "https://www.google.com/search?udm=2&q=" + + encodeURIComponent(String(name)), + ) + } + > + + Bilder + + + WebBrowser.openBrowserAsync( + "https://www.google.com/search?q=" + + encodeURIComponent(`${String(name)} Instagram`), + ) + } + > + + Instagram + + + - - - WebBrowser.openBrowserAsync( - "https://www.google.com/search?udm=2&q=" + - encodeURIComponent(String(name)) - ) - } - > - - + {/* Loading */} + {isLoading && ( + + + + )} - Auftritte: + {/* Appearances */} + {!isLoading && appearances.length > 0 && ( + + Auftritte - - {appearances.toReversed().map(({ show, seasons }) => { - const partners = Array.from( - new Map( - seasons - .map((s) => s.partner) - .filter((p): p is NonNullable => !!p) - .map((p) => [p.personId, p]) - ).values() - ); + + {appearances.toReversed().map(({ show, seasons }) => { + const partners = Array.from( + new Map( + seasons + .map((s) => s.partner) + .filter((p): p is NonNullable => !!p) + .map((p) => [p.personId, p]), + ).values(), + ); - const allParticipants = Array.from( - new Map( - seasons - .flatMap((s) => s.participants) - .filter((p) => p.personId !== pid) - .map((p) => [p.personId, p]) - ).values() - ); + const allParticipants = Array.from( + new Map( + seasons + .flatMap((s) => s.participants) + .filter((p) => p.personId !== pid) + .map((p) => [p.personId, p]), + ).values(), + ); - const isExpanded = expandedShows.has(show.id); - const visible = isExpanded - ? allParticipants - : allParticipants.slice(0, 12); - const restCount = Math.max( - allParticipants.length - visible.length, - 0 - ); + const isExpanded = expandedShows.has(show.id); + const visible = isExpanded + ? allParticipants + : allParticipants.slice(0, 12); + const restCount = Math.max( + allParticipants.length - visible.length, + 0, + ); - return ( - - goToShow(show.id)} - > - - - - {show.title} - - - ({formatYear(seasons[0]?.startDate)}) - - - Staffel {seasons.map((s) => s.seasonNumber).join(" und ")} - + return ( + + goToShow(show.id)} + > + + - - - - Weitere Teilnehmer - - - - - {visible.map((p) => ( - goToPerson(p)} - > - - {p.name} + + + + + {show.title} - - ))} - - {!isExpanded && restCount > 0 && ( - toggleExpand(show.id)} - style={styles.moreChip} - > - - +{restCount} mehr + + Staffel{" "} + {seasons.map((s) => s.seasonNumber).join(" & ")} + {" · "} + {formatYear(seasons[0]?.startDate)} + + + WebBrowser.openBrowserAsync( + "https://www.google.com/search?udm=2&q=" + + encodeURIComponent( + `${String(name)} ${show.title}`, + ), + ) + } + > + + + + {partners.length > 0 && ( + + Partner + + {partners.map((p) => ( + goToPerson(p)} + > + + + {p.name} + + + ))} + + )} - {isExpanded && allParticipants.length > 12 && ( - toggleExpand(show.id)} - style={styles.moreChip} - > - Weniger - + {allParticipants.length > 0 && ( + + + Weitere Teilnehmer + + + {visible.map((p) => ( + goToPerson(p)} + > + + {p.name} + + + ))} + + {!isExpanded && restCount > 0 && ( + toggleExpand(show.id)} + style={styles.moreChip} + > + + +{restCount} mehr + + + )} + + {isExpanded && allParticipants.length > 12 && ( + toggleExpand(show.id)} + style={styles.moreChip} + > + Weniger + + )} + + )} - - {partners.length > 0 && ( - <> - - - Partner - - - - - {partners.map((p) => ( - - {p.name} - - ))} - - )} - - ); - })} - - + ); + })} + + + )} ); diff --git a/app/showDetails.tsx b/app/showDetails.tsx index ce82174..0ff3422 100644 --- a/app/showDetails.tsx +++ b/app/showDetails.tsx @@ -1,6 +1,5 @@ import ParticipantDetails from "@/components/ui/ParticipantDeatails"; import ShowInfo from "@/components/ui/ShowInfo"; -import StackHeader from "@/components/ui/StackHeader"; import { useSeasonCount, useSeasonDates, @@ -11,9 +10,11 @@ import * as Haptics from "expo-haptics"; import { router, useLocalSearchParams } from "expo-router"; import React from "react"; import { + ActivityIndicator, Dimensions, Image, ScrollView, + StyleSheet, Text, TouchableOpacity, View, @@ -21,15 +22,17 @@ import { import styles from "./stackStyles/showDetailStyles"; export default function ShowDetails() { - const { id } = useLocalSearchParams(); + const { id, logoUri } = useLocalSearchParams(); const showId = Number(id); + const logoUriString = Array.isArray(logoUri) ? logoUri[0] : logoUri; const [selectedParticipants, setSelectedParticipants] = React.useState(true); const [selectedSeason, setSelectedSeason] = React.useState(1); - const { data: show } = useShow(showId); - const { data: seasonCount = 0 } = useSeasonCount(showId); + const { data: show, isLoading: showLoading } = useShow(showId); + const { data: seasonCount = 0, isLoading: seasonCountLoading } = + useSeasonCount(showId); const { data: participants, isLoading: pLoading, @@ -40,7 +43,7 @@ export default function ShowDetails() { const sortedParticipants = React.useMemo(() => { return [...participants].sort((a, b) => - a.name.localeCompare(b.name, "de", { sensitivity: "base" }) + a.name.localeCompare(b.name, "de", { sensitivity: "base" }), ); }, [participants]); @@ -62,156 +65,227 @@ export default function ShowDetails() { }, [startDate]); const handleOpenParticipant = React.useCallback( - (p: { id: number; name: string }) => { + (p: { id: number; name: string; imageUri?: string }) => { router.push({ pathname: "/participant", params: { participantId: p.id, name: p.name, + imageUri: p.imageUri || "", originShowId: String(showId), originSeason: String(selectedSeason), }, }); }, - [showId, selectedSeason] + [showId, selectedSeason], ); + const isInitialLoading = showLoading || seasonCountLoading; + return ( - - - {formattedStartDate ? ( - {formattedStartDate} - ) : null} - - - - + {isInitialLoading ? ( + + - - setSelectedParticipants(true)}> - - Teilnehmer - - - setSelectedParticipants(false)}> - - Details - - - - {selectedParticipants ? ( - <> - - Staffeln - - {Array.from({ length: seasonCount }, (_, idx) => idx + 1).map( - (season) => ( - { - setSelectedSeason(season); - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); - }} - > - {season} - - ) - )} - + ) : ( + + {logoUriString ? ( + + - - - {pError && ( - - {pError} - - )} - {!pLoading && !pError && participants.length === 0 && ( - Keine Teilnehmer. - )} - {sortedParticipants.map((p) => ( - handleOpenParticipant(p)} - > - - - {p.name} - - - ))} - - - ) : ( - {formattedStartDate} + ) : null} + - )} - + + + + + + setSelectedParticipants(true)} + style={{ + backgroundColor: selectedParticipants + ? "rgba(25,158,219,0.2)" + : "transparent", + borderRadius: 20, + borderWidth: selectedParticipants + ? StyleSheet.hairlineWidth + : 0, + borderColor: "rgba(25,158,219,0.4)", + }} + > + + Teilnehmer + + + setSelectedParticipants(false)} + style={{ + backgroundColor: !selectedParticipants + ? "rgba(25,158,219,0.2)" + : "transparent", + borderRadius: 20, + borderWidth: !selectedParticipants + ? StyleSheet.hairlineWidth + : 0, + borderColor: "rgba(25,158,219,0.4)", + }} + > + + Details + + + + {selectedParticipants ? ( + <> + + Staffeln + + {Array.from({ length: seasonCount }, (_, idx) => idx + 1).map( + (season) => ( + { + setSelectedSeason(season); + Haptics.impactAsync( + Haptics.ImpactFeedbackStyle.Light, + ); + }} + > + {season} + + ), + )} + + + + + {pError && ( + + {pError} + + )} + {pLoading && ( + + + + )} + {!pLoading && !pError && participants.length === 0 && ( + + Keine Teilnehmer. + + )} + {!pLoading && + sortedParticipants.map((p) => ( + handleOpenParticipant(p)} + > + + + + + {p.name} + + + ))} + + + ) : ( + + )} + + )} ); } diff --git a/app/stackStyles/participantStyles.tsx b/app/stackStyles/participantStyles.tsx index 4ffb0c0..9f9d378 100644 --- a/app/stackStyles/participantStyles.tsx +++ b/app/stackStyles/participantStyles.tsx @@ -1,22 +1,49 @@ -import { Dimensions, StyleSheet } from "react-native"; import { Colors } from "@/constants/colors"; +import { Dimensions, StyleSheet } from "react-native"; const styles = StyleSheet.create({ mainContainer: { flex: 1, - backgroundColor: Colors.header, + backgroundColor: Colors.background, + paddingTop: 20, }, - closeIcon: { - position: "absolute", - top: Dimensions.get("window").height * 0.065, - right: 15, + profileHero: { + alignItems: "center", + paddingTop: 8, + paddingBottom: 20, + }, + profileImageContainer: { + width: 120, + height: 120, + borderRadius: 60, + overflow: "hidden", + borderWidth: 3, + borderColor: "rgba(25,158,219,0.4)", + shadowColor: "#000", + shadowOffset: { width: 0, height: 6 }, + shadowOpacity: 0.3, + shadowRadius: 12, + elevation: 8, + }, + profileImage: { + width: "100%", + height: "100%", }, participantName: { color: Colors.text, - fontSize: 20, - fontWeight: "600", + fontSize: 24, + fontWeight: "700", textAlign: "center", - marginTop: Dimensions.get("window").height * 0.06, + marginTop: 16, + letterSpacing: 0.3, + }, + participantSubtitle: { + color: "rgba(255,255,255,0.5)", + fontSize: 14, + fontWeight: "500", + textAlign: "center", + marginTop: 4, + letterSpacing: 0.2, }, participantImage: { width: "100%", @@ -36,36 +63,34 @@ const styles = StyleSheet.create({ marginTop: 5, }, participantInfo: { - color: Colors.textSecondary, - fontSize: 16, + color: "rgba(255,255,255,0.6)", + fontSize: 15, textAlign: "center", }, dot: { width: 4, height: 4, - borderRadius: 3, - backgroundColor: Colors.textSecondary, + borderRadius: 2, + backgroundColor: "rgba(255,255,255,0.3)", marginHorizontal: 7, - marginTop: 2, }, performedShowsSection: { width: "100%", - height: "100%", - backgroundColor: Colors.background, - marginTop: 20, + backgroundColor: "transparent", + paddingBottom: 40, }, performedShowsTitle: { - fontSize: 16, - fontWeight: "600", - color: Colors.textSecondary, - marginTop: 15, - marginLeft: 15, + fontSize: 18, + fontWeight: "700", + color: Colors.text, + marginTop: 8, + marginLeft: 16, + marginBottom: 4, + letterSpacing: 0.2, }, - showImage: { width: "100%", height: "100%", - borderRadius: 10, }, showLabel: { color: Colors.text, @@ -85,102 +110,165 @@ const styles = StyleSheet.create({ }, showTitle: { color: Colors.text, - fontSize: 12, - fontWeight: "600", - textAlign: "center", - marginTop: 15, + fontSize: 16, + fontWeight: "700", + letterSpacing: 0.1, }, showSeason: { - color: Colors.textSecondary, - fontSize: 12, - fontWeight: "400", - textAlign: "center", - marginTop: 5, + color: "rgba(255,255,255,0.45)", + fontSize: 13, + fontWeight: "500", }, showContainer: { - width: Dimensions.get("window").width - 75, - height: 200, - borderRadius: 15, - marginTop: 20, - alignItems: "center", - backgroundColor: Colors.primary, + width: Dimensions.get("window").width - 64, + height: 180, + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + overflow: "hidden", }, - card: { - width: Dimensions.get("window").width - 75, - alignItems: "center", + width: Dimensions.get("window").width - 64, + borderRadius: 20, + backgroundColor: "rgba(255,255,255,0.06)", + borderWidth: StyleSheet.hairlineWidth, + borderColor: "rgba(255,255,255,0.08)", + overflow: "hidden", + shadowColor: "#000", + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.2, + shadowRadius: 10, + elevation: 4, }, - - horizontalLine: { - height: 50, - width: 2, - backgroundColor: Colors.textSecondary, - marginTop: 10, - alignSelf: "center", + cardInfo: { + padding: 16, + gap: 4, }, - partnerLabel: { - color: Colors.textSecondary, - fontSize: 12, - fontWeight: "400", - textAlign: "center", - marginTop: 10, + cardTitleRow: { + flexDirection: "row", + alignItems: "flex-start", + gap: 12, }, - participantContainer: { - width: "auto", - minHeight: "auto", - borderRadius: 15, - marginTop: 15, - alignItems: "center", + cardSearchButton: { + width: 34, + height: 34, + borderRadius: 17, + backgroundColor: "rgba(25,158,219,0.15)", + borderWidth: StyleSheet.hairlineWidth, + borderColor: "rgba(25,158,219,0.25)", justifyContent: "center", - backgroundColor: Colors.header, - padding: 10, + alignItems: "center", + marginTop: 2, }, - - participantLabel: { - color: Colors.text, - fontSize: 12, + sectionLabel: { + color: "rgba(255,255,255,0.45)", + fontSize: 11, + fontWeight: "600", + letterSpacing: 0.4, + textTransform: "uppercase", + marginBottom: 8, + }, + partnerSection: { + marginTop: 14, + paddingTop: 14, + borderTopWidth: StyleSheet.hairlineWidth, + borderTopColor: "rgba(255,255,255,0.08)", + }, + partnerRow: { + flexDirection: "row", + flexWrap: "wrap", + gap: 8, + }, + partnerChip: { + flexDirection: "row", + alignItems: "center", + gap: 6, + paddingVertical: 6, + paddingHorizontal: 14, + borderRadius: 20, + backgroundColor: "rgba(231,76,139,0.15)", + borderWidth: StyleSheet.hairlineWidth, + borderColor: "rgba(231,76,139,0.3)", + }, + partnerChipText: { + color: "rgba(255,255,255,0.9)", + fontSize: 13, + fontWeight: "600", + }, + participantsSection: { + marginTop: 14, + paddingTop: 14, + borderTopWidth: StyleSheet.hairlineWidth, + borderTopColor: "rgba(255,255,255,0.08)", }, participantRow: { flexDirection: "row", flexWrap: "wrap", gap: 6, - - alignItems: "center", - justifyContent: "flex-start", }, - participantChip: { - paddingVertical: 4, - paddingHorizontal: 8, - borderRadius: 12, - backgroundColor: "hsl(221, 39%, 18%)", + paddingVertical: 5, + paddingHorizontal: 10, + borderRadius: 14, + backgroundColor: "rgba(255,255,255,0.08)", maxWidth: 160, }, participantChipText: { - color: "hsl(0, 0%, 85%)", + color: "rgba(255,255,255,0.7)", fontSize: 11, + fontWeight: "500", }, - moreChip: { - paddingVertical: 4, - paddingHorizontal: 10, - borderRadius: 12, - backgroundColor: "hsl(221, 39%, 28%)", + paddingVertical: 5, + paddingHorizontal: 12, + borderRadius: 16, + backgroundColor: "rgba(25,158,219,0.2)", + borderWidth: StyleSheet.hairlineWidth, + borderColor: "rgba(25,158,219,0.3)", }, moreChipText: { - color: Colors.text, + color: "#199edb", fontSize: 11, fontWeight: "600", }, + heroButtons: { + flexDirection: "row", + gap: 10, + marginTop: 16, + }, searchButton: { - width: 50, - height: 50, - borderRadius: 20, - backgroundColor: Colors.header, - marginLeft: 15, - marginTop: 15, - marginBottom: 5, - justifyContent: "center", + flexDirection: "row", + alignItems: "center", + gap: 8, + paddingVertical: 10, + paddingHorizontal: 18, + borderRadius: 22, + backgroundColor: "rgba(25,158,219,0.15)", + borderWidth: StyleSheet.hairlineWidth, + borderColor: "rgba(25,158,219,0.3)", + }, + searchButtonText: { + color: "#199edb", + fontSize: 14, + fontWeight: "600", + }, + instagramButton: { + flexDirection: "row", + alignItems: "center", + gap: 8, + paddingVertical: 10, + paddingHorizontal: 18, + borderRadius: 22, + backgroundColor: "rgba(225,48,108,0.12)", + borderWidth: StyleSheet.hairlineWidth, + borderColor: "rgba(225,48,108,0.3)", + }, + instagramButtonText: { + color: "#E1306C", + fontSize: 14, + fontWeight: "600", + }, + loadingContainer: { + paddingVertical: 60, alignItems: "center", }, }); diff --git a/app/stackStyles/showDetailStyles.tsx b/app/stackStyles/showDetailStyles.tsx index d1345f8..862990e 100644 --- a/app/stackStyles/showDetailStyles.tsx +++ b/app/stackStyles/showDetailStyles.tsx @@ -1,10 +1,20 @@ -import { StyleSheet } from "react-native"; import { Colors } from "@/constants/colors"; +import { Dimensions, StyleSheet } from "react-native"; const styles = StyleSheet.create({ mainContainer: { flex: 1, - backgroundColor: Colors.header, + backgroundColor: Colors.background, + paddingTop: Dimensions.get("screen").height * 0.1, + }, + logoContainer: { + alignItems: "center", + paddingTop: 8, + paddingBottom: 4, + }, + showLogo: { + width: 100, + height: 80, }, showImage: { width: 200, @@ -14,136 +24,168 @@ const styles = StyleSheet.create({ bottom: 10, }, showMainInfoSection: { - width: "auto", - height: "auto", alignSelf: "center", flexDirection: "row", justifyContent: "center", alignItems: "center", - bottom: 25, + gap: 8, + marginBottom: 8, }, showInfoText: { - color: Colors.textSecondary, - fontSize: 14, + color: "rgba(255,255,255,0.7)", + fontSize: 13, + fontWeight: "500", }, dot: { width: 4, height: 4, - borderRadius: 3, - backgroundColor: Colors.textSecondary, - marginHorizontal: 7, - marginTop: 2, + borderRadius: 2, + backgroundColor: "rgba(255,255,255,0.3)", + marginHorizontal: 6, }, showBannerLogoContainer: { width: "100%", - height: 200, + height: 220, alignSelf: "center", - borderTopLeftRadius: 80, - borderTopRightRadius: 80, - marginTop: 15, + marginTop: 8, }, showBannerLogo: { width: "100%", height: "100%", - borderTopLeftRadius: 30, - borderTopRightRadius: 30, + borderTopLeftRadius: 28, + borderTopRightRadius: 28, }, infoContainner: { width: "100%", minHeight: "auto", paddingHorizontal: 20, - paddingVertical: 15, - backgroundColor: Colors.background, + paddingVertical: 14, + backgroundColor: "transparent", flexDirection: "row", - gap: 20, + gap: 6, }, infoLabel: { - fontWeight: "300", - color: Colors.textSecondary, - fontSize: 16, + fontWeight: "500", + color: "rgba(255,255,255,0.5)", + fontSize: 15, + paddingHorizontal: 16, + paddingVertical: 8, + borderRadius: 20, + overflow: "hidden", + letterSpacing: 0.3, }, participantsDetailsContainer: { width: "100%", - height: "100%", - backgroundColor: Colors.card, + minHeight: 200, + backgroundColor: "transparent", + }, + participantWrapper: { + width: (Dimensions.get("window").width - 32 - 24) / 3, + alignItems: "center", + marginBottom: 16, }, participantContainer: { - height: 160, - width: 110, - backgroundColor: Colors.primary, - borderRadius: 10, - marginBottom: 30, + width: "100%", + aspectRatio: 0.72, + borderRadius: 16, + overflow: "hidden", + shadowColor: "#000", + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.2, + shadowRadius: 8, + elevation: 4, }, participantSection: { flexDirection: "row", flexWrap: "wrap", - gap: 15, - paddingLeft: 15, - paddingTop: 15, + gap: 12, + paddingHorizontal: 16, + paddingTop: 12, }, seasonsSection: { width: "100%", - minHeight: 40, - backgroundColor: Colors.card, + minHeight: 50, + backgroundColor: "transparent", flexDirection: "row", alignItems: "center", - gap: 10, + gap: 12, paddingHorizontal: 20, + paddingVertical: 8, }, seasonList: { flexDirection: "row", alignItems: "center", - gap: 10, - paddingLeft: 5, - paddingRight: 5, + gap: 8, + paddingLeft: 4, + paddingRight: 8, }, seasonContainer: { - width: 35, - height: 35, - borderRadius: 5, - backgroundColor: "hsl(0, 0%, 20%)", + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: "rgba(255,255,255,0.08)", justifyContent: "center", alignItems: "center", + borderWidth: StyleSheet.hairlineWidth, + borderColor: "rgba(255,255,255,0.06)", }, seasonLabel: { color: Colors.text, - fontWeight: "bold", + fontWeight: "700", + fontSize: 14, }, participantLabel: { color: Colors.text, - fontWeight: "500", + fontWeight: "600", textAlign: "center", - fontSize: 11, - marginTop: 10, + fontSize: 12, + marginTop: 6, + letterSpacing: 0.1, + width: "100%", }, seasonsLabel: { - color: Colors.textSecondary, - fontWeight: "500", - fontSize: 16, + color: "rgba(255,255,255,0.6)", + fontWeight: "600", + fontSize: 15, + letterSpacing: 0.2, }, detailTitle: { - color: Colors.text, - fontSize: 14, - fontWeight: "bold", + color: "rgba(255,255,255,0.95)", + fontSize: 15, + fontWeight: "700", marginTop: 10, marginLeft: 20, marginBottom: 5, + letterSpacing: 0.2, }, detailLabel: { - color: Colors.textSecondary, + color: "rgba(255,255,255,0.6)", fontSize: 14, - lineHeight: 20, + lineHeight: 22, width: "90%", - fontWeight: "300", + fontWeight: "400", marginLeft: 20, marginTop: 5, }, startDate: { - color: Colors.textSecondary, - fontSize: 16, + color: "rgba(255,255,255,0.5)", + fontSize: 14, textAlign: "center", - marginTop: 15, - fontStyle: "italic", + marginTop: 14, + fontWeight: "500", + letterSpacing: 0.5, + textTransform: "uppercase", + }, + loadingContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + }, + sectionLoading: { + width: "100%", + paddingVertical: 40, + justifyContent: "center", + alignItems: "center", }, }); export default styles; diff --git a/app/tabStyles/indexStyles.tsx b/app/tabStyles/indexStyles.tsx index 70cefc7..5d21656 100644 --- a/app/tabStyles/indexStyles.tsx +++ b/app/tabStyles/indexStyles.tsx @@ -16,7 +16,7 @@ export default StyleSheet.create({ mainContainer: { flex: 1, backgroundColor: Colors.background, - paddingHorizontal: 5, + paddingHorizontal: 10, }, header: { minHeight: 125, diff --git a/components/discovery/PersonRow.tsx b/components/discovery/PersonRow.tsx index 12f39e8..ff6f0e8 100644 --- a/components/discovery/PersonRow.tsx +++ b/components/discovery/PersonRow.tsx @@ -1,7 +1,7 @@ -import { FontAwesome } from "@expo/vector-icons"; +import Feather from "@expo/vector-icons/Feather"; import { router } from "expo-router"; import React from "react"; -import { StyleSheet, Text, TouchableOpacity, View } from "react-native"; +import { Image, StyleSheet, Text, TouchableOpacity, View } from "react-native"; export type PersonLite = { id?: number; @@ -24,66 +24,110 @@ const calcAge = (birthDate?: string | null): number | null => { type Props = { person: any; - onPress?: () => void; + isFirst?: boolean; + isLast?: boolean; }; -export default function PersonRow({ person }: Props) { +export default function PersonRow({ person, isFirst, isLast }: Props) { const age = calcAge(person.birthDate); const id = person.personId ?? person.id; + const imageUrl = person.imageUrl ?? person.imageUri ?? null; + const isPravatar = imageUrl?.includes("pravatar"); const goToPerson = React.useCallback( (id: number) => { - console.log("go to person", id); router.push({ pathname: "/participant", - params: { participantId: String(id), name: person.name }, + params: { + participantId: String(id), + name: person.name, + imageUri: imageUrl || "", + }, }); }, - [person.name] + [person.name, imageUrl], ); return ( { - goToPerson(Number(id)); - }} - style={styles.personRow} + onPress={() => goToPerson(Number(id))} + style={[ + styles.personRow, + isFirst && styles.firstRow, + isLast && styles.lastRow, + ]} + activeOpacity={0.6} > - + {imageUrl && !isPravatar ? ( + + ) : ( + + )} - - - {person.name || "Unbekannt"} - {age != null ? ` (${age})` : ""} - - {/* aus: unterschiedlichen Shows */} + + + {person.name || "Unbekannt"} + {age != null && {age} Jahre} + + - ); } const styles = StyleSheet.create({ personRow: { - width: "100%", flexDirection: "row", alignItems: "center", - backgroundColor: "#1b1e2b", - borderRadius: 10, - paddingHorizontal: 10, - paddingVertical: 10, - marginBottom: 8, + backgroundColor: "rgba(255,255,255,0.06)", + paddingLeft: 16, + minHeight: 56, + }, + firstRow: { + borderTopLeftRadius: 10, + borderTopRightRadius: 10, + }, + lastRow: { + borderBottomLeftRadius: 10, + borderBottomRightRadius: 10, }, avatarCircle: { - width: 40, - height: 40, - borderRadius: 999, - backgroundColor: "#2a2f45", + width: 36, + height: 36, + borderRadius: 18, + backgroundColor: "rgba(255,255,255,0.1)", alignItems: "center", justifyContent: "center", - marginRight: 10, + marginRight: 12, + overflow: "hidden", + }, + avatarImage: { + width: 36, + height: 36, + borderRadius: 18, + }, + content: { + flex: 1, + flexDirection: "row", + alignItems: "center", + paddingRight: 16, + paddingVertical: 12, + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: "rgba(255,255,255,0.08)", + }, + personName: { + color: "white", + fontSize: 17, + fontWeight: "400", + }, + personMeta: { + color: "rgba(255,255,255,0.5)", + fontSize: 14, + marginTop: 1, }, - personName: { color: "white", fontSize: 16, fontWeight: "600" }, - personMeta: { color: "#bbb", fontSize: 12, marginTop: 2 }, }); diff --git a/components/discovery/TagChip.tsx b/components/discovery/TagChip.tsx index a851358..4d896da 100644 --- a/components/discovery/TagChip.tsx +++ b/components/discovery/TagChip.tsx @@ -1,20 +1,45 @@ -import { FontAwesome } from "@expo/vector-icons"; +import Feather from "@expo/vector-icons/Feather"; import React from "react"; import { StyleSheet, Text, TouchableOpacity, View } from "react-native"; -export default function TagChip({ icon, label, onPress }: { icon: any; label: string; onPress: () => void }) { +export default function TagChip({ + icon: _icon, + label, + onPress, +}: { + icon: any; + label: string; + onPress: () => void; +}) { return ( - + - {label} - + ); } const styles = StyleSheet.create({ - tag: { flexDirection: "row", alignItems: "center", backgroundColor: "#333", borderRadius: 999, paddingHorizontal: 10, paddingVertical: 6, marginRight: 8, marginBottom: 8 }, - tagLabel: { color: "white" }, + tag: { + flexDirection: "row", + alignItems: "center", + backgroundColor: "rgba(255,255,255,0.12)", + borderRadius: 8, + paddingHorizontal: 12, + paddingVertical: 7, + marginRight: 8, + marginBottom: 8, + }, + tagLabel: { + color: "white", + fontSize: 15, + fontWeight: "400", + }, }); diff --git a/components/ui/ParticipantDeatails.tsx b/components/ui/ParticipantDeatails.tsx index 2b4c8f7..1df33f3 100644 --- a/components/ui/ParticipantDeatails.tsx +++ b/components/ui/ParticipantDeatails.tsx @@ -1,3 +1,4 @@ +import { BlurView } from "expo-blur"; import { StyleSheet, Text, View } from "react-native"; type ParticipantDetailsProps = { @@ -14,41 +15,77 @@ const ParticipantDetails = ({ streamingService, }: ParticipantDetailsProps) => { return ( - - Beschreibung: - {description} - Konzept: - {concept} - Genres: - {genres.join(', ')} - Produktion: - {streamingService} + + + Beschreibung + {description} + + + Konzept + {concept} + + + Genres + + {genres.map((g) => ( + + {g} + + ))} + + + + Produktion + {streamingService} + ); }; const styles = StyleSheet.create({ - participantsDetailsContainer: { + container: { width: "100%", - height: "100%", - backgroundColor: "hsl(221, 39%, 2%)", + paddingHorizontal: 16, + paddingTop: 12, + paddingBottom: 20, + gap: 12, + backgroundColor: "transparent", + }, + card: { + borderRadius: 20, + overflow: "hidden", + padding: 18, + borderWidth: StyleSheet.hairlineWidth, + borderColor: "rgba(255,255,255,0.08)", }, detailTitle: { - color: "hsl(0, 0%, 100%)", - fontSize: 14, - fontWeight: "bold", - marginTop: 10, - marginLeft: 20, - marginBottom: 5, + color: "rgba(255,255,255,0.95)", + fontSize: 15, + fontWeight: "700", + marginBottom: 8, + letterSpacing: 0.2, }, detailLabel: { - color: "hsl(0, 0%, 80%)", + color: "rgba(255,255,255,0.65)", fontSize: 14, - lineHeight: 20, - width: "90%", - fontWeight: "300", - marginLeft: 20, - marginTop: 5, + lineHeight: 22, + fontWeight: "400", + }, + genreRow: { + flexDirection: "row", + flexWrap: "wrap", + gap: 8, + }, + genrePill: { + backgroundColor: "rgba(255,255,255,0.1)", + paddingHorizontal: 14, + paddingVertical: 6, + borderRadius: 16, + }, + genrePillText: { + color: "rgba(255,255,255,0.8)", + fontSize: 13, + fontWeight: "500", }, }); diff --git a/components/ui/ShowCard.tsx b/components/ui/ShowCard.tsx index 3147720..7bed1ce 100644 --- a/components/ui/ShowCard.tsx +++ b/components/ui/ShowCard.tsx @@ -5,7 +5,6 @@ type ShowCardProps = { imageUri: string; streamingServicesUris: string[]; liveBadgeText?: string; - liveBadgeContainerStyle?: object; genres: string[]; title: string; onPress?: () => void; @@ -15,139 +14,144 @@ const ShowCard = ({ imageUri, streamingServicesUris, liveBadgeText, - liveBadgeContainerStyle, genres, onPress, title, }: ShowCardProps) => { return ( - + - - {streamingServicesUris.length > 0 && streamingServicesUris.map((service) => ( - + {/* Gradient-like overlay at bottom */} + - ))} + {/* Streaming service icons */} + + {streamingServicesUris.length > 0 && + streamingServicesUris.map((service) => ( + + ))} + {/* Live badge */} {liveBadgeText && ( - + + {liveBadgeText} )} - - + {/* Bottom info */} + + {title} - - - {genres.map((genre) => ( - - {genre} - - ))} + {genres.length > 0 && ( + + {genres.slice(0, 3).map((genre) => ( + + {genre} + + ))} + + )} ); }; const styles = StyleSheet.create({ - showContainer: { + card: { width: "100%", - height: 220, + height: 200, + borderRadius: 18, + marginTop: 14, + overflow: "hidden", + backgroundColor: "rgba(255,255,255,0.06)", + }, + bottomGradient: { + ...StyleSheet.absoluteFillObject, + borderRadius: 18, backgroundColor: "transparent", - alignSelf: "center", - borderRadius: 35, - marginTop: 20, - borderWidth: 1.5, - borderColor: "hsl(221, 39%, 15%)", - shadowColor: "#000", - shadowOffset: { - width: 0, - height: 1, - }, - shadowOpacity: 0.18, - shadowRadius: 1.0, - elevation: 1, + // A dark gradient from bottom for readability + // Using a semi-transparent overlay at bottom }, - streamingServiceIcon: { - width: 45, - height: 45, - marginLeft: "auto", - marginRight: 15, - borderWidth: 1, - borderColor: "white", - borderRadius: 15, - marginTop: 15, + serviceRow: { + flexDirection: "row", + justifyContent: "flex-end", + padding: 10, + gap: 6, }, - liveBadgeContainer: { + serviceIcon: { + height: 34, + width: 34, + borderRadius: 17, + resizeMode: "contain", + backgroundColor: "rgba(0,0,0,0.3)", + }, + liveBadge: { position: "absolute", - top: 15, - left: 20, - backgroundColor: "red", - borderRadius: 10, - paddingVertical: 5, + top: 12, + left: 12, + flexDirection: "row", + alignItems: "center", + gap: 5, + backgroundColor: "rgba(0,0,0,0.55)", + paddingVertical: 4, paddingHorizontal: 10, + borderRadius: 12, + }, + liveDot: { + width: 7, + height: 7, + borderRadius: 4, + backgroundColor: "#ff3b30", }, liveBadgeText: { color: "white", - fontWeight: "bold", + fontWeight: "700", + fontSize: 11, + letterSpacing: 0.5, }, - genreSection: { + bottomInfo: { position: "absolute", - bottom: 15, - left: 20, + bottom: 0, + left: 0, + right: 0, + paddingHorizontal: 14, + paddingBottom: 12, + paddingTop: 24, + backgroundColor: "rgba(0,0,0,0.45)", + }, + title: { + color: "white", + fontWeight: "700", + fontSize: 16, + letterSpacing: 0.2, + }, + genreRow: { flexDirection: "row", - alignItems: "center", - justifyContent: "space-evenly", - gap: 5, + gap: 6, + marginTop: 5, + flexWrap: "wrap", }, - genreLabel: { - color: "red", - fontWeight: "bold", - fontSize: 10, - paddingVertical: 5, - paddingHorizontal: 10, - borderRadius: 10, - fontStyle: "italic", - backgroundColor: "rgba(255, 255, 255, 1)", + genreTag: { + color: "rgba(255,255,255,0.8)", + fontSize: 11, + fontWeight: "500", + paddingVertical: 2, + paddingHorizontal: 8, + borderRadius: 8, + backgroundColor: "rgba(255,255,255,0.15)", overflow: "hidden", }, - titleSection: { - width: "auto", - height: 45, - paddingHorizontal: 20, - backgroundColor: "rgba(0, 0, 0, 0.6)", - position: "absolute", - top: 50, - justifyContent: "center", - alignItems: "flex-start", - borderTopRightRadius: 15, - borderBottomRightRadius: 15, - }, }); export default ShowCard; diff --git a/components/ui/ShowInfo.tsx b/components/ui/ShowInfo.tsx index 9493e97..1d83b03 100644 --- a/components/ui/ShowInfo.tsx +++ b/components/ui/ShowInfo.tsx @@ -1,4 +1,5 @@ -import { View, Text, StyleSheet } from "react-native"; +import { BlurView } from "expo-blur"; +import { StyleSheet, Text, View } from "react-native"; type ShowInfoProps = { seasons: number; @@ -17,37 +18,42 @@ const ShowInfo = ({ }: ShowInfoProps) => { return ( - {seasons} Staffeln - - {participants} Teilnehmer - - {streamingService} + + {seasons} Staffeln + + + {participants} Teilnehmer + + + {streamingService} + ); }; const styles = StyleSheet.create({ showMainInfoSection: { - width: "auto", - height: "auto", alignSelf: "center", flexDirection: "row", justifyContent: "center", alignItems: "center", - top: 20, - marginBottom: 20, + gap: 8, + marginTop: 8, + marginBottom: 12, + }, + pill: { + paddingHorizontal: 14, + paddingVertical: 7, + borderRadius: 20, + overflow: "hidden", + borderWidth: StyleSheet.hairlineWidth, + borderColor: "rgba(255,255,255,0.1)", }, showInfoText: { - color: "hsl(0, 0%, 80%)", - fontSize: 14, - }, - dot: { - width: 4, - height: 4, - borderRadius: 3, - backgroundColor: "hsl(0, 0%, 80%)", - marginHorizontal: 7, - marginTop: 2, + color: "rgba(255,255,255,0.85)", + fontSize: 13, + fontWeight: "500", + letterSpacing: 0.2, }, }); diff --git a/components/ui/StackHeader.tsx b/components/ui/StackHeader.tsx index 0d5b995..825801b 100644 --- a/components/ui/StackHeader.tsx +++ b/components/ui/StackHeader.tsx @@ -1,4 +1,5 @@ import Feather from "@expo/vector-icons/Feather"; +import { BlurView } from "expo-blur"; import { router, useLocalSearchParams } from "expo-router"; import React from "react"; import { @@ -15,48 +16,51 @@ export default function StackHeader() { const logoUriString = Array.isArray(logoUri) ? logoUri[0] : logoUri; return ( - + router.back()}> - + + + - {/* - - */} - + + ); } const styles = StyleSheet.create({ header: { - height: 150, - backgroundColor: "hsl(221, 39%, 12%)", + height: 140, alignItems: "center", justifyContent: "space-between", flexDirection: "row", - borderBottomWidth: 1, - paddingTop: Dimensions.get("window").height * 0.065, - paddingHorizontal: 20, - - borderBottomColor: "hsl(221, 39%, 15%)", - shadowColor: "#000", - shadowOffset: { - width: 0, - height: 3, - }, - shadowOpacity: 0.25, - shadowRadius: 3.84, - elevation: 5, + paddingTop: Dimensions.get("window").height * 0.06, + paddingHorizontal: 16, + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: "rgba(255,255,255,0.08)", + }, + backButton: { + width: 40, + height: 40, + borderRadius: 20, + overflow: "hidden", + justifyContent: "center", + alignItems: "center", + borderWidth: StyleSheet.hairlineWidth, + borderColor: "rgba(255,255,255,0.18)", }, logo: { width: 100, height: 100, resizeMode: "contain", - marginLeft: 10, }, title: { color: "white", fontSize: 14, - fontWeight: "bold", + fontWeight: "600", }, }); diff --git a/hooks/useSearch.ts b/hooks/useSearch.ts index 82b0c03..9e3b1ec 100644 --- a/hooks/useSearch.ts +++ b/hooks/useSearch.ts @@ -1,10 +1,10 @@ +import { discoverSearch } from "@/apis/searchApi"; import { useQuery } from "@tanstack/react-query"; -import { getSearchResults } from "@/apis/searchApi"; export const useSearch = (tags: string[]) => { return useQuery({ queryKey: ["search", tags], - queryFn: () => getSearchResults(tags), + queryFn: () => discoverSearch(tags), enabled: tags.length > 0, }); -}; \ No newline at end of file +}; diff --git a/utils/searchMapping.ts b/utils/searchMapping.ts index ae904f3..b369691 100644 --- a/utils/searchMapping.ts +++ b/utils/searchMapping.ts @@ -29,7 +29,7 @@ export function mapApiPersonToUI(data: any) { export function mapApiSeasonToUI(data: any) { return { seasonId: data?.seasonId ?? data?.id, - showId: data?.showId, + showId: data?.showId ?? data?.show, startDate: data?.startDate ?? null, endDate: data?.endDate ?? null, seasonNumber: data?.seasonNumber ?? null, @@ -40,12 +40,20 @@ export function mapApiSeasonToUI(data: any) { } export function mapApiShowToUI(data: any) { + const id = data?.showId ?? data?.id; + const genre = data?.genre ?? ""; return { - showId: data?.showId ?? data?.id, - title: data?.title ?? data?.name ?? `Show #${data?.showId ?? data?.id ?? "?"}`, + id, + showId: id, + title: data?.title ?? data?.name ?? `Show #${id ?? "?"}`, description: data?.description ?? "", - genre: data?.genre ?? "", - thumbnailUrl: data?.thumbnailUrl ?? data?.imageUrl ?? "", + genres: genre ? genre.split(",").map((g: string) => g.trim()) : [], + genre, + thumbnailUri: data?.thumbnailUrl ?? data?.imageUrl ?? "", + bannerUri: data?.bannerUrl ?? "", + streamingService: data?.streamingServices ?? "", + concept: data?.concept ?? "", running: data?.running ?? false, + logoUrl: data?.logoUrl ?? "", } as any; }