modified: files to ios26 ui/ux
This commit is contained in:
@@ -7,22 +7,27 @@ export type SearchResultItem = {
|
|||||||
|
|
||||||
const DISCOVER_BASE = "https://fltr-app.de/api/discover/search";
|
const DISCOVER_BASE = "https://fltr-app.de/api/discover/search";
|
||||||
|
|
||||||
export async function getSearchResults(
|
export async function discoverSearch(
|
||||||
tags: string[] | string,
|
tags: string[],
|
||||||
limit = 10,
|
signal?: AbortSignal,
|
||||||
signal?: AbortSignal
|
|
||||||
): Promise<SearchResultItem[]> {
|
): Promise<SearchResultItem[]> {
|
||||||
const tagList = Array.isArray(tags) ? tags : [tags];
|
const filteredTags = tags.map((t) => t.trim()).filter(Boolean);
|
||||||
const filteredTags = tagList.map((t) => t.trim()).filter(Boolean);
|
|
||||||
if (!filteredTags.length) return [];
|
if (!filteredTags.length) return [];
|
||||||
|
|
||||||
const url = `${DISCOVER_BASE}?tags=${encodeURIComponent(
|
const params = filteredTags
|
||||||
filteredTags.join(",")
|
.map((tag) => `tags=${encodeURIComponent(tag)}`)
|
||||||
)}&limit=${limit}`;
|
.join("&");
|
||||||
|
const url = `${DISCOVER_BASE}?${params}`;
|
||||||
|
|
||||||
const apiKey = process.env.EXPO_PUBLIC_API_KEY;
|
const apiKey = process.env.EXPO_PUBLIC_API_KEY;
|
||||||
const res = await fetch(url, { signal, headers: { 'Content-Type': 'application/json', "X-API-Key": apiKey ?? "", } });
|
const res = await fetch(url, {
|
||||||
if (!res.ok) throw new Error("AutoComplete failed " + res.status);
|
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();
|
const data: unknown = await res.json();
|
||||||
if (!Array.isArray(data)) return [];
|
if (!Array.isArray(data)) return [];
|
||||||
|
|||||||
@@ -1,40 +1,45 @@
|
|||||||
import Feather from "@expo/vector-icons/Feather";
|
import Feather from "@expo/vector-icons/Feather";
|
||||||
import { Tabs } from "expo-router";
|
import * as Haptics from "expo-haptics";
|
||||||
import React from "react";
|
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() {
|
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 (
|
return (
|
||||||
<Tabs
|
<NativeTabs>
|
||||||
screenOptions={{
|
<NativeTabs.Trigger name="home">
|
||||||
headerShown: false,
|
<Label>Home</Label>
|
||||||
tabBarActiveTintColor: "#dc2626",
|
<Icon
|
||||||
tabBarStyle: {
|
sf={{ default: "house", selected: "house.fill" }}
|
||||||
backgroundColor: "hsl(221, 39%, 12%)",
|
androidSrc={<VectorIcon family={Feather} name="home" />}
|
||||||
borderTopColor: "#dc2626",
|
/>
|
||||||
borderTopWidth: 1.5,
|
</NativeTabs.Trigger>
|
||||||
paddingTop: 10,
|
<NativeTabs.Trigger name="explore">
|
||||||
},
|
<Label>Durchsuchen</Label>
|
||||||
tabBarInactiveTintColor: "hsl(0, 0%, 100%)",
|
<Icon
|
||||||
}}
|
sf="magnifyingglass"
|
||||||
>
|
androidSrc={<VectorIcon family={Feather} name="search" />}
|
||||||
<Tabs.Screen
|
/>
|
||||||
name="index"
|
</NativeTabs.Trigger>
|
||||||
options={{
|
</NativeTabs>
|
||||||
title: "Home",
|
|
||||||
tabBarIcon: ({ color, size }) => (
|
|
||||||
<Feather name="home" size={size} color={color} />
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Tabs.Screen
|
|
||||||
name="explore"
|
|
||||||
options={{
|
|
||||||
title: "Durchsuchen",
|
|
||||||
tabBarIcon: ({ color, size }) => (
|
|
||||||
<Feather name="search" size={size} color={color} />
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tabs>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<AutoCompleteItem[]>([]);
|
|
||||||
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<Record<number, Show>>({});
|
|
||||||
|
|
||||||
// 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<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]);
|
|
||||||
|
|
||||||
// SEASON-Ergebnisse nach showId gruppieren
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
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<number, Show> = {};
|
|
||||||
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 (
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.mainContainer,
|
|
||||||
{ justifyContent: "center", alignItems: "center", padding: 20 },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 12,
|
|
||||||
backgroundColor: "rgba(255,255,255,0.05)",
|
|
||||||
paddingVertical: 24,
|
|
||||||
paddingHorizontal: 20,
|
|
||||||
borderRadius: 12,
|
|
||||||
width: "85%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text style={{ fontSize: 36 }}>⚠️</Text>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: "600",
|
|
||||||
color: "white",
|
|
||||||
textAlign: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Fehler beim Laden
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
fontSize: 14,
|
|
||||||
color: "rgba(255,255,255,0.6)",
|
|
||||||
textAlign: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{error?.message || "Ein unerwarteter Fehler ist aufgetreten."}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => {
|
|
||||||
if (typeof refetch === "function") refetch();
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
marginTop: 6,
|
|
||||||
backgroundColor: "rgba(255,255,255,0.15)",
|
|
||||||
paddingVertical: 10,
|
|
||||||
paddingHorizontal: 18,
|
|
||||||
borderRadius: 8,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text style={{ color: "white", fontWeight: "600" }}>Erneut versuchen</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const noResults = persons.length === 0 && seasonsByShowId.size === 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={[styles.mainContainer]}>
|
|
||||||
<View style={styles.header}>
|
|
||||||
<Text style={[styles.title, { fontSize: 28 }]}>Durchsuchen</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<TouchableWithoutFeedback
|
|
||||||
onPress={() => {
|
|
||||||
Keyboard.dismiss();
|
|
||||||
setShowSuggestions(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<View style={{ flex: 1 }}>
|
|
||||||
<View style={styles.sectionContainer}>
|
|
||||||
<View style={styles.searchContainer}>
|
|
||||||
<TextInput
|
|
||||||
value={query}
|
|
||||||
autoComplete="off"
|
|
||||||
autoCorrect={false}
|
|
||||||
onChangeText={(t) => {
|
|
||||||
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 ? (
|
|
||||||
<Feather name="search" size={24} color="hsl(221, 39%, 80%)" />
|
|
||||||
) : (
|
|
||||||
<Feather
|
|
||||||
name="x"
|
|
||||||
size={24}
|
|
||||||
color="hsl(221, 39%, 80%)"
|
|
||||||
onPress={() => {
|
|
||||||
setQuery("");
|
|
||||||
setShowSuggestions(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<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 && showSuggestions && (
|
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<View style={styles.sectionContainer}>
|
|
||||||
{seasonsByShowId.size > 0 && (
|
|
||||||
<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) {
|
|
||||||
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} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{/* Schöner Empty-State */}
|
|
||||||
{noResults && (
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
gap: 12,
|
|
||||||
backgroundColor: "rgba(255,255,255,0.04)",
|
|
||||||
paddingVertical: 28,
|
|
||||||
paddingHorizontal: 20,
|
|
||||||
borderRadius: 12,
|
|
||||||
marginTop: 8,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Feather
|
|
||||||
name="search"
|
|
||||||
size={36}
|
|
||||||
color="rgba(255,255,255,0.9)"
|
|
||||||
/>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: "600",
|
|
||||||
color: "white",
|
|
||||||
textAlign: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Keine Ergebnisse gefunden
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
fontSize: 14,
|
|
||||||
color: "rgba(255,255,255,0.7)",
|
|
||||||
textAlign: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Passen Sie Ihre Tags an oder setzen Sie die Filter zurück.
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<View style={{ flexDirection: "row", gap: 10, marginTop: 4 }}>
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => {
|
|
||||||
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,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text style={{ color: "white", fontWeight: "600" }}>
|
|
||||||
Filter zurücksetzen
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => {
|
|
||||||
setQuery("");
|
|
||||||
setShowSuggestions(false);
|
|
||||||
Keyboard.dismiss();
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
backgroundColor: "rgba(255,255,255,0.08)",
|
|
||||||
paddingVertical: 10,
|
|
||||||
paddingHorizontal: 14,
|
|
||||||
borderRadius: 8,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text style={{ color: "white" }}>Eingabe löschen</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</ScrollView>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</TouchableWithoutFeedback>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
34
app/(tabs)/explore/_layout.tsx
Normal file
34
app/(tabs)/explore/_layout.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Colors } from "@/constants/colors";
|
||||||
|
import { Stack } from "expo-router";
|
||||||
|
|
||||||
|
export default function ExploreLayout() {
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
screenOptions={{
|
||||||
|
headerStyle: { backgroundColor: Colors.background },
|
||||||
|
headerTintColor: Colors.text,
|
||||||
|
headerTitleStyle: {
|
||||||
|
fontSize: 17,
|
||||||
|
fontWeight: "600",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack.Screen
|
||||||
|
name="index"
|
||||||
|
options={{
|
||||||
|
title: "Durchsuchen",
|
||||||
|
headerLargeTitle: true,
|
||||||
|
headerLargeTitleStyle: {
|
||||||
|
color: Colors.text,
|
||||||
|
fontSize: 28,
|
||||||
|
},
|
||||||
|
headerSearchBarOptions: {
|
||||||
|
placeholder: "Wonach suchst du?",
|
||||||
|
hideWhenScrolling: false,
|
||||||
|
autoCapitalize: "none",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
579
app/(tabs)/explore/index.tsx
Normal file
579
app/(tabs)/explore/index.tsx
Normal file
@@ -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<AutoCompleteItem[]>([]);
|
||||||
|
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<Record<number, Show>>({});
|
||||||
|
|
||||||
|
// 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<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]);
|
||||||
|
|
||||||
|
// SEASON-Ergebnisse nach showId gruppieren
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
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<number, Show> = {};
|
||||||
|
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<number, { show: Show; seasons: Season[] }>();
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
s.container,
|
||||||
|
{ justifyContent: "center", alignItems: "center", padding: 20 },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<View style={s.emptyState}>
|
||||||
|
<Feather
|
||||||
|
name="alert-triangle"
|
||||||
|
size={44}
|
||||||
|
color="rgba(255,255,255,0.2)"
|
||||||
|
/>
|
||||||
|
<Text style={s.emptyTitle}>Fehler beim Laden</Text>
|
||||||
|
<Text style={s.emptySubtitle}>
|
||||||
|
{error?.message || "Ein unerwarteter Fehler ist aufgetreten."}
|
||||||
|
</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => {
|
||||||
|
if (typeof refetch === "function") refetch();
|
||||||
|
}}
|
||||||
|
style={s.emptyButton}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<Text style={s.emptyButtonText}>Erneut versuchen</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const noResults = persons.length === 0 && unifiedShows.length === 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={s.container}>
|
||||||
|
<Stack.Screen
|
||||||
|
options={{
|
||||||
|
headerSearchBarOptions: {
|
||||||
|
placeholder: "Wonach suchst du?",
|
||||||
|
hideWhenScrolling: false,
|
||||||
|
autoCapitalize: "none",
|
||||||
|
onChangeText: (e) => {
|
||||||
|
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 && (
|
||||||
|
<View style={s.suggestionsOverlay}>
|
||||||
|
<ScrollView
|
||||||
|
keyboardShouldPersistTaps="handled"
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
<Text style={s.suggestionsHeader}>VORSCHLÄGE</Text>
|
||||||
|
<View style={s.suggestionsGroup}>
|
||||||
|
{suggestions.slice(0, 10).map((suggestion, idx, arr) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={suggestion.text + "_" + idx}
|
||||||
|
style={[
|
||||||
|
s.suggestionRow,
|
||||||
|
idx === 0 && {
|
||||||
|
borderTopLeftRadius: 10,
|
||||||
|
borderTopRightRadius: 10,
|
||||||
|
},
|
||||||
|
idx === arr.length - 1 && {
|
||||||
|
borderBottomLeftRadius: 10,
|
||||||
|
borderBottomRightRadius: 10,
|
||||||
|
borderBottomWidth: 0,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onPress={() => tagAdded(suggestion)}
|
||||||
|
activeOpacity={0.6}
|
||||||
|
>
|
||||||
|
<Feather
|
||||||
|
name={
|
||||||
|
suggestion.type === "PERSON"
|
||||||
|
? "user"
|
||||||
|
: suggestion.type === "SHOW"
|
||||||
|
? "tv"
|
||||||
|
: "tag"
|
||||||
|
}
|
||||||
|
size={14}
|
||||||
|
color="rgba(255,255,255,0.5)"
|
||||||
|
/>
|
||||||
|
<Text style={s.suggestionText}>{suggestion.text}</Text>
|
||||||
|
<Feather
|
||||||
|
name="arrow-up-left"
|
||||||
|
size={12}
|
||||||
|
color="rgba(255,255,255,0.3)"
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
contentInsetAdjustmentBehavior="automatic"
|
||||||
|
keyboardShouldPersistTaps="handled"
|
||||||
|
>
|
||||||
|
{/* Tag chips */}
|
||||||
|
{tags.length > 0 && (
|
||||||
|
<View style={s.section}>
|
||||||
|
<View style={s.tagRow}>
|
||||||
|
{tags.map((tag) => (
|
||||||
|
<TagChip
|
||||||
|
key={tag.text}
|
||||||
|
icon={getIconName(tag.type)}
|
||||||
|
label={tag.text}
|
||||||
|
onPress={() => tagRemoved(tag)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Personen Section */}
|
||||||
|
{persons.length > 0 && (
|
||||||
|
<View style={s.section}>
|
||||||
|
<Text style={s.sectionHeader}>PERSONEN</Text>
|
||||||
|
<View style={s.groupedCard}>
|
||||||
|
{persons.slice(0, 5).map((p, i, arr) => (
|
||||||
|
<PersonRow
|
||||||
|
key={`p-${p.personId ?? p.id}`}
|
||||||
|
person={p}
|
||||||
|
isFirst={i === 0}
|
||||||
|
isLast={i === arr.length - 1}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Shows + Staffeln unified */}
|
||||||
|
{unifiedShows.length > 0 && (
|
||||||
|
<View style={s.section}>
|
||||||
|
<Text style={s.sectionHeader}>SHOWS</Text>
|
||||||
|
{unifiedShows.map(({ show, seasons }) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={`show-${show.id}`}
|
||||||
|
style={s.showCard}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
onPress={() =>
|
||||||
|
router.push({
|
||||||
|
pathname: "/showDetails",
|
||||||
|
params: { id: String(show.id) },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
source={{
|
||||||
|
uri: show.bannerUri || show.thumbnailUri,
|
||||||
|
}}
|
||||||
|
style={s.showCardImage}
|
||||||
|
resizeMode="cover"
|
||||||
|
/>
|
||||||
|
<View style={s.showCardBody}>
|
||||||
|
<View style={s.showCardHeader}>
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<Text style={s.showCardTitle} numberOfLines={1}>
|
||||||
|
{show.title}
|
||||||
|
</Text>
|
||||||
|
{show.description ? (
|
||||||
|
<Text style={s.showCardDescription} numberOfLines={2}>
|
||||||
|
{show.description}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
{show.running && (
|
||||||
|
<View style={s.liveBadge}>
|
||||||
|
<Text style={s.liveBadgeText}>LIVE</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
{seasons.length > 0 && (
|
||||||
|
<View style={s.seasonRow}>
|
||||||
|
{seasons.map((season) => (
|
||||||
|
<View
|
||||||
|
key={(season as any).seasonId}
|
||||||
|
style={s.seasonPill}
|
||||||
|
>
|
||||||
|
<Text style={s.seasonPillText}>
|
||||||
|
S{(season as any).seasonNumber}
|
||||||
|
{(season as any).startDate
|
||||||
|
? ` · ${new Date((season as any).startDate).getFullYear()}`
|
||||||
|
: ""}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Empty State */}
|
||||||
|
{noResults && (
|
||||||
|
<View style={s.section}>
|
||||||
|
<View style={s.emptyState}>
|
||||||
|
<Feather name="search" size={44} color="rgba(255,255,255,0.2)" />
|
||||||
|
<Text style={s.emptyTitle}>Keine Ergebnisse</Text>
|
||||||
|
<Text style={s.emptySubtitle}>
|
||||||
|
Passe deine Tags an oder setze die Filter zurück.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{tags.length > 0 && (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => {
|
||||||
|
setTags([]);
|
||||||
|
setQuery("");
|
||||||
|
setShowSuggestions(false);
|
||||||
|
if (typeof refetch === "function") refetch();
|
||||||
|
}}
|
||||||
|
style={s.emptyButton}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<Text style={s.emptyButtonText}>Filter zurücksetzen</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
});
|
||||||
30
app/(tabs)/home/_layout.tsx
Normal file
30
app/(tabs)/home/_layout.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { Colors } from "@/constants/colors";
|
||||||
|
import { Stack } from "expo-router";
|
||||||
|
|
||||||
|
export default function HomeLayout() {
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
screenOptions={{
|
||||||
|
headerStyle: { backgroundColor: Colors.background },
|
||||||
|
headerTintColor: Colors.text,
|
||||||
|
headerTitleStyle: {
|
||||||
|
fontSize: 17,
|
||||||
|
fontWeight: "600",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack.Screen
|
||||||
|
name="index"
|
||||||
|
options={{
|
||||||
|
title: "FLTR",
|
||||||
|
headerLargeTitle: true,
|
||||||
|
headerLargeTitleStyle: {
|
||||||
|
color: Colors.text,
|
||||||
|
fontSize: 34,
|
||||||
|
fontWeight: "800",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
353
app/(tabs)/home/index.tsx
Normal file
353
app/(tabs)/home/index.tsx
Normal file
@@ -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<string>("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<string>();
|
||||||
|
shows.forEach((show) => {
|
||||||
|
show.streamingService
|
||||||
|
.split(", ")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.forEach((service) => uniqueServices.add(service));
|
||||||
|
});
|
||||||
|
return Array.from(uniqueServices);
|
||||||
|
}, [shows]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<View style={s.centered}>
|
||||||
|
<ActivityIndicator size="large" color="rgba(255,255,255,0.6)" />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<View style={s.centered}>
|
||||||
|
<View style={s.errorCard}>
|
||||||
|
<Text style={{ fontSize: 36 }}>⚠️</Text>
|
||||||
|
<Text style={s.errorTitle}>Fehler beim Laden</Text>
|
||||||
|
<Text style={s.errorMessage}>
|
||||||
|
{error?.message || "Ein unerwarteter Fehler ist aufgetreten."}
|
||||||
|
</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => {
|
||||||
|
if (typeof refetchShows === "function") refetchShows();
|
||||||
|
if (typeof refetchServices === "function") refetchServices();
|
||||||
|
}}
|
||||||
|
style={s.retryButton}
|
||||||
|
>
|
||||||
|
<Text style={s.retryText}>Erneut versuchen</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={s.container}>
|
||||||
|
<Stack.Screen
|
||||||
|
options={{
|
||||||
|
headerRight: () => (
|
||||||
|
<TouchableOpacity onPress={() => router.push("/legal")} hitSlop={8}>
|
||||||
|
<Feather
|
||||||
|
name="info"
|
||||||
|
size={22}
|
||||||
|
color={Platform.OS === "ios" ? Colors.primary : Colors.text}
|
||||||
|
style={{ left: "32.5%" }}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
contentInsetAdjustmentBehavior="automatic"
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={refreshing}
|
||||||
|
onRefresh={onRefresh}
|
||||||
|
tintColor="rgba(255,255,255,0.6)"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{/* Filter chips */}
|
||||||
|
<ScrollView
|
||||||
|
horizontal
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
contentContainerStyle={s.filterRow}
|
||||||
|
>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[s.filterPill, activeFilter === "all" && s.filterPillActive]}
|
||||||
|
onPress={() => handleFilter("all")}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
s.filterPillText,
|
||||||
|
activeFilter === "all" && s.filterPillTextActive,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
Alle
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
s.filterPill,
|
||||||
|
activeFilter === "live" && s.filterPillActive,
|
||||||
|
]}
|
||||||
|
onPress={() => handleFilter("live")}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<View style={s.liveDot} />
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
s.filterPillText,
|
||||||
|
activeFilter === "live" && s.filterPillTextActive,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
Live
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{uniqueStreamingServices.map((serviceName) => {
|
||||||
|
const serviceUri =
|
||||||
|
streamingServices[
|
||||||
|
`assets.images.streamingServices.${serviceName.toLowerCase()}`
|
||||||
|
];
|
||||||
|
const isActive = activeFilter === serviceName;
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={serviceName}
|
||||||
|
style={[s.serviceChip, isActive && s.serviceChipActive]}
|
||||||
|
onPress={() => handleFilter(serviceName)}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
{serviceUri ? (
|
||||||
|
<Image source={{ uri: serviceUri }} style={s.serviceIcon} />
|
||||||
|
) : (
|
||||||
|
<Text style={s.filterPillText}>{serviceName}</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* Show cards */}
|
||||||
|
<View style={s.cardList}>
|
||||||
|
{filteredShows.map((show) => (
|
||||||
|
<ShowCard
|
||||||
|
key={show.id}
|
||||||
|
title={show.title}
|
||||||
|
onPress={() =>
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
: {})}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,356 +1,5 @@
|
|||||||
import styles from "@/app/tabStyles/indexStyles";
|
import { Redirect } from "expo-router";
|
||||||
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";
|
|
||||||
|
|
||||||
export default function HomeScreen() {
|
export default function () {
|
||||||
const {
|
return <Redirect href="/home" />;
|
||||||
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<string>("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<string>();
|
|
||||||
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 (
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.mainContainer,
|
|
||||||
{ justifyContent: "center", alignItems: "center" },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<ActivityIndicator size="large" color="#ffffff" />
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.mainContainer,
|
|
||||||
{ justifyContent: "center", alignItems: "center", padding: 20 },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 12,
|
|
||||||
backgroundColor: "rgba(255,255,255,0.05)",
|
|
||||||
paddingVertical: 24,
|
|
||||||
paddingHorizontal: 20,
|
|
||||||
borderRadius: 12,
|
|
||||||
width: "85%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text style={{ fontSize: 36 }}>⚠️</Text>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: "600",
|
|
||||||
color: "white",
|
|
||||||
textAlign: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Fehler beim Laden
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
fontSize: 14,
|
|
||||||
color: "rgba(255,255,255,0.6)",
|
|
||||||
textAlign: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{error?.message || "Ein unerwarteter Fehler ist aufgetreten."}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => {
|
|
||||||
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,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text style={{ color: "white", fontWeight: "600" }}>
|
|
||||||
Erneut versuchen
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<GestureHandlerRootView>
|
|
||||||
<View style={styles.mainContainer}>
|
|
||||||
<View style={styles.header}>
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => {
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
<Feather name="info" size={22} color="#FFFFFF" />
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<Text style={styles.title}>FLTR</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<RNScrollView
|
|
||||||
contentContainerStyle={{ paddingBottom: 30 }}
|
|
||||||
showsVerticalScrollIndicator={false}
|
|
||||||
refreshControl={
|
|
||||||
<RefreshControl
|
|
||||||
refreshing={refreshing}
|
|
||||||
onRefresh={onRefresh}
|
|
||||||
tintColor="#FFFFFF" // iOS Spinner
|
|
||||||
colors={["#FFFFFF"]} // Android Spinner
|
|
||||||
progressBackgroundColor="hsla(0, 0%, 29%, 1.00)"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<View style={styles.filterSection}>
|
|
||||||
{/* ⬅️ HORIZONTALER SCROLLBEREICH BLEIBT AUS RNGH */}
|
|
||||||
<ScrollView
|
|
||||||
horizontal
|
|
||||||
showsHorizontalScrollIndicator={false}
|
|
||||||
contentContainerStyle={{
|
|
||||||
alignItems: "center",
|
|
||||||
paddingHorizontal: 10,
|
|
||||||
gap: 10,
|
|
||||||
marginLeft: 10,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{activeFilter !== "all" && (
|
|
||||||
<TouchableOpacity
|
|
||||||
style={{
|
|
||||||
padding: 5,
|
|
||||||
height: 60,
|
|
||||||
width: 60,
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
backgroundColor: "hsla(0, 0%, 29%, 1.00)",
|
|
||||||
borderRadius: 50,
|
|
||||||
}}
|
|
||||||
onPress={() => handleFilter("all")}
|
|
||||||
>
|
|
||||||
<Text style={{ fontWeight: "bold", color: "white" }}>
|
|
||||||
ALLE
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
{activeFilter !== "live" && (
|
|
||||||
<TouchableOpacity
|
|
||||||
style={{
|
|
||||||
padding: 5,
|
|
||||||
height: 60,
|
|
||||||
width: 60,
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
backgroundColor: "hsla(0, 0%, 29%, 1.00)",
|
|
||||||
borderRadius: 50,
|
|
||||||
}}
|
|
||||||
onPress={() => handleFilter("live")}
|
|
||||||
>
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
backgroundColor: "red",
|
|
||||||
paddingHorizontal: 5,
|
|
||||||
paddingVertical: 2,
|
|
||||||
borderRadius: 5,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text style={{ fontWeight: "bold", color: "white" }}>
|
|
||||||
LIVE
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
height: 60,
|
|
||||||
width: 2,
|
|
||||||
backgroundColor: "hsla(0, 0%, 37%, 1.00)",
|
|
||||||
marginHorizontal: 5,
|
|
||||||
borderRadius: 5,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{uniqueStreamingServices.map((serviceName) => {
|
|
||||||
const streamingService =
|
|
||||||
streamingServices[
|
|
||||||
`assets.images.streamingServices.${serviceName.toLowerCase()}`
|
|
||||||
];
|
|
||||||
return (
|
|
||||||
<TouchableOpacity
|
|
||||||
key={serviceName}
|
|
||||||
style={{
|
|
||||||
padding: 5,
|
|
||||||
backgroundColor: "hsla(0, 0%, 29%, 1.00)",
|
|
||||||
borderRadius: 50,
|
|
||||||
borderWidth: serviceName.includes(activeFilter) ? 2 : 0,
|
|
||||||
borderColor: "hsla(0, 100%, 50%, 1.00)",
|
|
||||||
}}
|
|
||||||
onPress={() => handleFilter(serviceName)}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
source={{ uri: streamingService }}
|
|
||||||
style={{
|
|
||||||
width: 50,
|
|
||||||
height: 50,
|
|
||||||
borderRadius: 30,
|
|
||||||
resizeMode: "contain",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ScrollView>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={{ flex: 1, paddingHorizontal: 10 }}>
|
|
||||||
{filteredShows.map((show) => {
|
|
||||||
const showLiveBadge = show.running;
|
|
||||||
return (
|
|
||||||
<ShowCard
|
|
||||||
key={show.id}
|
|
||||||
title={show.title}
|
|
||||||
onPress={() =>
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
: {})}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</View>
|
|
||||||
</RNScrollView>
|
|
||||||
</View>
|
|
||||||
</GestureHandlerRootView>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Colors } from "@/constants/colors";
|
||||||
import { DiscoveryProvider } from "@/contexts/DiscoveryContext";
|
import { DiscoveryProvider } from "@/contexts/DiscoveryContext";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { Stack } from "expo-router";
|
import { Stack } from "expo-router";
|
||||||
@@ -9,28 +10,46 @@ export default function RootLayout() {
|
|||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<DiscoveryProvider>
|
<DiscoveryProvider>
|
||||||
<Stack>
|
<Stack
|
||||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
screenOptions={{
|
||||||
|
headerStyle: { backgroundColor: Colors.background },
|
||||||
|
headerTintColor: "#199edb",
|
||||||
|
headerTitleStyle: { color: "white" },
|
||||||
|
headerBackTitle: "",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack.Screen
|
||||||
|
name="(tabs)"
|
||||||
|
options={{ headerShown: false, headerBackTitle: "" }}
|
||||||
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="showDetails"
|
name="showDetails"
|
||||||
options={{
|
options={{
|
||||||
headerShown: false,
|
headerShown: true,
|
||||||
|
headerTransparent: true,
|
||||||
|
headerBlurEffect: "dark",
|
||||||
|
title: "",
|
||||||
|
headerBackButtonDisplayMode: "minimal",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="participant"
|
name="participant"
|
||||||
options={{
|
options={{
|
||||||
headerShown: false,
|
headerShown: true,
|
||||||
|
headerTransparent: true,
|
||||||
|
headerBlurEffect: "dark",
|
||||||
|
title: "",
|
||||||
|
headerBackButtonDisplayMode: "minimal",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="legal"
|
name="legal"
|
||||||
options={{
|
options={{
|
||||||
presentation: "modal",
|
presentation: "modal",
|
||||||
headerShown: false
|
headerShown: false,
|
||||||
}} />
|
}}
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
</DiscoveryProvider>
|
</DiscoveryProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
289
app/legal.tsx
289
app/legal.tsx
@@ -1,154 +1,227 @@
|
|||||||
|
import { Colors } from "@/constants/colors";
|
||||||
import Feather from "@expo/vector-icons/Feather";
|
import Feather from "@expo/vector-icons/Feather";
|
||||||
|
import { BlurView } from "expo-blur";
|
||||||
import { router } from "expo-router";
|
import { router } from "expo-router";
|
||||||
import React from "react";
|
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() {
|
export default function LegalScreen() {
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, backgroundColor: "hsl(220, 15%, 10%)" }}>
|
<View style={styles.container}>
|
||||||
|
{/* Header */}
|
||||||
|
<BlurView intensity={40} tint="dark" style={styles.header}>
|
||||||
<View
|
<Text style={styles.headerTitle}>Info</Text>
|
||||||
style={{
|
|
||||||
paddingTop: 18,
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
paddingBottom: 6,
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
color: "white",
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: "700",
|
|
||||||
letterSpacing: 0.3,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Info
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => router.back()}
|
onPress={() => router.back()}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Modal schließen"
|
accessibilityLabel="Modal schließen"
|
||||||
style={{
|
style={styles.closeButton}
|
||||||
height: 40,
|
|
||||||
width: 40,
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
borderRadius: 10,
|
|
||||||
backgroundColor: "rgba(255,255,255,0.08)",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Feather name="x" size={22} color="#FFFFFF" />
|
<Feather name="x" size={18} color="rgba(255,255,255,0.85)" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</BlurView>
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
contentContainerStyle={{
|
contentContainerStyle={styles.scrollContent}
|
||||||
paddingHorizontal: 16,
|
|
||||||
paddingBottom: 28,
|
|
||||||
}}
|
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
>
|
>
|
||||||
{/* Impressum Card */}
|
{/* Impressum */}
|
||||||
<View
|
<View style={styles.card}>
|
||||||
style={{
|
<View style={styles.cardHeader}>
|
||||||
backgroundColor: "rgba(255,255,255,0.05)",
|
<View style={styles.cardIconCircle}>
|
||||||
borderRadius: 14,
|
<Feather name="briefcase" size={16} color="#199edb" />
|
||||||
padding: 16,
|
</View>
|
||||||
gap: 10,
|
<Text style={styles.cardTitle}>Impressum</Text>
|
||||||
marginTop: 8,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text style={{ color: "white", fontSize: 18, fontWeight: "700" }}>
|
|
||||||
Impressum
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<View style={{ gap: 4 }}>
|
|
||||||
<Text style={styles.mono}>Berg Autosoft</Text>
|
|
||||||
<Text style={styles.mono}>Joe Felipe Berg</Text>
|
|
||||||
<Text style={styles.mono}>Stöckener Straße 35</Text>
|
|
||||||
<Text style={styles.mono}>30419 Hannover</Text>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={{ height: 8 }} />
|
<View style={styles.cardSection}>
|
||||||
|
<Text style={styles.textPrimary}>Berg Autosoft</Text>
|
||||||
<View style={{ gap: 4 }}>
|
<Text style={styles.textPrimary}>Joe Felipe Berg</Text>
|
||||||
<Text style={styles.dim}>+49 1522 5642948</Text>
|
<Text style={styles.textPrimary}>Stöckener Straße 35</Text>
|
||||||
<Text style={styles.dim}>kontakt@berg-autosoft.de</Text>
|
<Text style={styles.textPrimary}>30419 Hannover</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={{ height: 8 }} />
|
<View style={styles.divider} />
|
||||||
|
|
||||||
<View style={{ gap: 4 }}>
|
<View style={styles.cardSection}>
|
||||||
<Text style={styles.dim}>Steuernummer: 25/103/17193</Text>
|
<View style={styles.infoRow}>
|
||||||
<Text style={styles.dim}>USt-ID: DE361689728</Text>
|
<Feather name="phone" size={14} color="rgba(255,255,255,0.4)" />
|
||||||
|
<Text style={styles.textSecondary}>+49 1522 5642948</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.infoRow}>
|
||||||
|
<Feather name="mail" size={14} color="rgba(255,255,255,0.4)" />
|
||||||
|
<Text style={styles.textSecondary}>kontakt@berg-autosoft.de</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.divider} />
|
||||||
|
|
||||||
|
<View style={styles.cardSection}>
|
||||||
|
<Text style={styles.textDim}>Steuernummer: 25/103/17193</Text>
|
||||||
|
<Text style={styles.textDim}>USt-ID: DE361689728</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Support Card */}
|
{/* Support */}
|
||||||
<View
|
<View style={styles.card}>
|
||||||
style={{
|
<View style={styles.cardHeader}>
|
||||||
backgroundColor: "rgba(255,255,255,0.05)",
|
<View style={styles.cardIconCircle}>
|
||||||
borderRadius: 14,
|
<Feather name="headphones" size={16} color="#199edb" />
|
||||||
padding: 16,
|
</View>
|
||||||
gap: 10,
|
<Text style={styles.cardTitle}>Support</Text>
|
||||||
marginTop: 12,
|
</View>
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text style={{ color: "white", fontSize: 18, fontWeight: "700" }}>
|
|
||||||
Support
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text style={styles.body}>
|
<Text style={styles.textBody}>
|
||||||
Sollten Sie Probleme bei der Nutzung der iOS- oder Android-App FLTR
|
Sollten Sie Probleme bei der Nutzung der iOS- oder Android-App FLTR
|
||||||
haben, wenden Sie sich bitte direkt an den Support.
|
haben, wenden Sie sich bitte direkt an den Support.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<View style={{ height: 6 }} />
|
<View style={styles.divider} />
|
||||||
|
|
||||||
<Text style={styles.body}>Schreiben Sie eine E-Mail an:</Text>
|
<Text style={styles.textDim}>Schreiben Sie eine E-Mail an:</Text>
|
||||||
<Text style={[styles.mono, { fontSize: 15 }]}>
|
<View style={styles.emailPill}>
|
||||||
developer@berg-autosoft.de
|
<Feather name="mail" size={14} color="#199edb" />
|
||||||
</Text>
|
<Text style={styles.emailText}>developer@berg-autosoft.de</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
<Text style={[styles.dim, { marginTop: 10 }]}>
|
<Text style={[styles.textDim, { marginTop: 12 }]}>
|
||||||
Wir bemühen uns, Ihr Anliegen so schnell wie möglich zu bearbeiten.
|
Wir bemühen uns, Ihr Anliegen so schnell wie möglich zu bearbeiten.
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<View style={{ alignItems: "center", marginTop: 18 }}>
|
<Text style={styles.footer}>© 2025 Berg Autosoft</Text>
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
color: "rgba(255,255,255,0.6)",
|
|
||||||
fontSize: 12,
|
|
||||||
letterSpacing: 0.2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
© 2025 Berg Autosoft
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = {
|
const styles = StyleSheet.create({
|
||||||
mono: {
|
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)",
|
color: "rgba(255,255,255,0.92)",
|
||||||
fontSize: 16,
|
fontSize: 15,
|
||||||
} as const,
|
},
|
||||||
dim: {
|
textSecondary: {
|
||||||
color: "rgba(255,255,255,0.75)",
|
color: "rgba(255,255,255,0.75)",
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
} as const,
|
},
|
||||||
body: {
|
textDim: {
|
||||||
color: "rgba(255,255,255,0.88)",
|
color: "rgba(255,255,255,0.5)",
|
||||||
|
fontSize: 13,
|
||||||
|
},
|
||||||
|
textBody: {
|
||||||
|
color: "rgba(255,255,255,0.8)",
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
lineHeight: 20,
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,23 +1,32 @@
|
|||||||
import { PersonMini } from "@/apis/personHistoryApi";
|
import { PersonMini } from "@/apis/personHistoryApi";
|
||||||
import styles from "@/app/stackStyles/participantStyles";
|
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 Ionicons from "@expo/vector-icons/Ionicons";
|
||||||
import { router, useLocalSearchParams } from "expo-router";
|
import { router, useLocalSearchParams } from "expo-router";
|
||||||
import * as WebBrowser from "expo-web-browser";
|
import * as WebBrowser from "expo-web-browser";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Image, Text, TouchableOpacity, View } from "react-native";
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
|
Image,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from "react-native";
|
||||||
import {
|
import {
|
||||||
GestureHandlerRootView,
|
GestureHandlerRootView,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
} from "react-native-gesture-handler";
|
} from "react-native-gesture-handler";
|
||||||
|
|
||||||
export default function ParticipantScreen() {
|
export default function ParticipantScreen() {
|
||||||
const { name, participantId } = useLocalSearchParams();
|
const { name, participantId, imageUri } = useLocalSearchParams();
|
||||||
|
|
||||||
const pid = Array.isArray(participantId)
|
const pid = Array.isArray(participantId)
|
||||||
? Number(participantId[0])
|
? Number(participantId[0])
|
||||||
: Number(participantId);
|
: Number(participantId);
|
||||||
|
|
||||||
|
const imageUriString = Array.isArray(imageUri) ? imageUri[0] : imageUri;
|
||||||
|
const isPravatar = imageUriString?.includes("pravatar");
|
||||||
|
|
||||||
const { data: appearances = [], isLoading, isError } = usePersonHistory(pid);
|
const { data: appearances = [], isLoading, isError } = usePersonHistory(pid);
|
||||||
|
|
||||||
const formatYear = (iso?: string | null) => {
|
const formatYear = (iso?: string | null) => {
|
||||||
@@ -27,7 +36,7 @@ export default function ParticipantScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const [expandedShows, setExpandedShows] = React.useState<Set<number>>(
|
const [expandedShows, setExpandedShows] = React.useState<Set<number>>(
|
||||||
new Set()
|
new Set(),
|
||||||
);
|
);
|
||||||
const toggleExpand = React.useCallback((showId: number) => {
|
const toggleExpand = React.useCallback((showId: number) => {
|
||||||
setExpandedShows((prev) => {
|
setExpandedShows((prev) => {
|
||||||
@@ -45,193 +54,240 @@ export default function ParticipantScreen() {
|
|||||||
const goToPerson = React.useCallback(
|
const goToPerson = React.useCallback(
|
||||||
(p: PersonMini) => {
|
(p: PersonMini) => {
|
||||||
if (!p?.personId) return;
|
if (!p?.personId) return;
|
||||||
|
|
||||||
if (p.personId === pid) return;
|
if (p.personId === pid) return;
|
||||||
router.push({
|
router.push({
|
||||||
pathname: "/participant",
|
pathname: "/participant",
|
||||||
params: { participantId: String(p.personId), name: p.name },
|
params: {
|
||||||
|
participantId: String(p.personId),
|
||||||
|
name: p.name,
|
||||||
|
imageUri: p.imageUrl || "",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[pid]
|
[pid],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GestureHandlerRootView style={styles.mainContainer}>
|
<GestureHandlerRootView style={styles.mainContainer}>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
contentContainerStyle={{ paddingBottom: 20, paddingTop: 10}}
|
contentContainerStyle={{ paddingBottom: 40, paddingTop: 100 }}
|
||||||
|
|
||||||
>
|
>
|
||||||
<Text style={styles.participantName}>{name}</Text>
|
{/* Profile Hero */}
|
||||||
<TouchableOpacity
|
<View style={styles.profileHero}>
|
||||||
style={styles.closeIcon}
|
<View style={styles.profileImageContainer}>
|
||||||
onPress={() => router.back()}
|
<Image
|
||||||
>
|
source={{ uri: imageUriString || undefined }}
|
||||||
<Ionicons name="close-circle-outline" size={38} color="white" />
|
style={styles.profileImage}
|
||||||
</TouchableOpacity>
|
resizeMode="cover"
|
||||||
|
blurRadius={isPravatar ? 16 : 0}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.participantName}>{name}</Text>
|
||||||
|
<Text style={styles.participantSubtitle}>
|
||||||
|
{appearances.length}{" "}
|
||||||
|
{appearances.length === 1 ? "Auftritt" : "Auftritte"}
|
||||||
|
</Text>
|
||||||
|
<View style={styles.heroButtons}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.searchButton}
|
||||||
|
onPress={() =>
|
||||||
|
WebBrowser.openBrowserAsync(
|
||||||
|
"https://www.google.com/search?udm=2&q=" +
|
||||||
|
encodeURIComponent(String(name)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Ionicons name="images-outline" size={18} color="#199edb" />
|
||||||
|
<Text style={styles.searchButtonText}>Bilder</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.instagramButton}
|
||||||
|
onPress={() =>
|
||||||
|
WebBrowser.openBrowserAsync(
|
||||||
|
"https://www.google.com/search?q=" +
|
||||||
|
encodeURIComponent(`${String(name)} Instagram`),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Ionicons name="logo-instagram" size={18} color="#E1306C" />
|
||||||
|
<Text style={styles.instagramButtonText}>Instagram</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
<View style={styles.performedShowsSection}>
|
{/* Loading */}
|
||||||
<TouchableOpacity
|
{isLoading && (
|
||||||
style={styles.searchButton}
|
<View style={styles.loadingContainer}>
|
||||||
onPress={() =>
|
<ActivityIndicator size="large" color="#199edb" />
|
||||||
WebBrowser.openBrowserAsync(
|
</View>
|
||||||
"https://www.google.com/search?udm=2&q=" +
|
)}
|
||||||
encodeURIComponent(String(name))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Ionicons name="images-outline" size={24} color="white" />
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<Text style={styles.performedShowsTitle}>Auftritte:</Text>
|
{/* Appearances */}
|
||||||
|
{!isLoading && appearances.length > 0 && (
|
||||||
|
<View style={styles.performedShowsSection}>
|
||||||
|
<Text style={styles.performedShowsTitle}>Auftritte</Text>
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
horizontal
|
horizontal
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
contentContainerStyle={{
|
contentContainerStyle={{
|
||||||
gap: 20,
|
gap: 20,
|
||||||
paddingHorizontal: 15,
|
paddingHorizontal: 16,
|
||||||
paddingLeft: 30,
|
paddingTop: 12,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{appearances.toReversed().map(({ show, seasons }) => {
|
{appearances.toReversed().map(({ show, seasons }) => {
|
||||||
const partners = Array.from(
|
const partners = Array.from(
|
||||||
new Map(
|
new Map(
|
||||||
seasons
|
seasons
|
||||||
.map((s) => s.partner)
|
.map((s) => s.partner)
|
||||||
.filter((p): p is NonNullable<typeof p> => !!p)
|
.filter((p): p is NonNullable<typeof p> => !!p)
|
||||||
.map((p) => [p.personId, p])
|
.map((p) => [p.personId, p]),
|
||||||
).values()
|
).values(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const allParticipants = Array.from(
|
const allParticipants = Array.from(
|
||||||
new Map(
|
new Map(
|
||||||
seasons
|
seasons
|
||||||
.flatMap((s) => s.participants)
|
.flatMap((s) => s.participants)
|
||||||
.filter((p) => p.personId !== pid)
|
.filter((p) => p.personId !== pid)
|
||||||
.map((p) => [p.personId, p])
|
.map((p) => [p.personId, p]),
|
||||||
).values()
|
).values(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const isExpanded = expandedShows.has(show.id);
|
const isExpanded = expandedShows.has(show.id);
|
||||||
const visible = isExpanded
|
const visible = isExpanded
|
||||||
? allParticipants
|
? allParticipants
|
||||||
: allParticipants.slice(0, 12);
|
: allParticipants.slice(0, 12);
|
||||||
const restCount = Math.max(
|
const restCount = Math.max(
|
||||||
allParticipants.length - visible.length,
|
allParticipants.length - visible.length,
|
||||||
0
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View key={show.id} style={styles.card}>
|
<View key={show.id} style={styles.card}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.showContainer}
|
style={styles.showContainer}
|
||||||
onPress={() => goToShow(show.id)}
|
onPress={() => goToShow(show.id)}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: show.bannerUri || show.thumbnailUri }}
|
source={{ uri: show.bannerUri || show.thumbnailUri }}
|
||||||
style={styles.showImage}
|
style={styles.showImage}
|
||||||
resizeMode="cover"
|
resizeMode="cover"
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Text style={styles.showTitle} numberOfLines={1}>
|
|
||||||
{show.title}
|
|
||||||
</Text>
|
|
||||||
<Text style={styles.showSeason}>
|
|
||||||
({formatYear(seasons[0]?.startDate)})
|
|
||||||
</Text>
|
|
||||||
<Text style={styles.showSeason}>
|
|
||||||
Staffel {seasons.map((s) => s.seasonNumber).join(" und ")}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<View style={styles.horizontalLine} />
|
<View style={styles.cardInfo}>
|
||||||
|
<View style={styles.cardTitleRow}>
|
||||||
<Text style={[styles.participantLabel, { marginTop: 10 }]}>
|
<View style={{ flex: 1 }}>
|
||||||
Weitere Teilnehmer
|
<Text style={styles.showTitle} numberOfLines={1}>
|
||||||
</Text>
|
{show.title}
|
||||||
|
|
||||||
<View style={styles.participantContainer}>
|
|
||||||
<View style={styles.participantRow}>
|
|
||||||
{visible.map((p) => (
|
|
||||||
<TouchableOpacity
|
|
||||||
key={p.personId}
|
|
||||||
style={styles.participantChip}
|
|
||||||
onPress={() => goToPerson(p)}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={styles.participantChipText}
|
|
||||||
numberOfLines={1}
|
|
||||||
>
|
|
||||||
{p.name}
|
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
<Text style={styles.showSeason}>
|
||||||
))}
|
Staffel{" "}
|
||||||
|
{seasons.map((s) => s.seasonNumber).join(" & ")}
|
||||||
{!isExpanded && restCount > 0 && (
|
{" · "}
|
||||||
<TouchableOpacity
|
{formatYear(seasons[0]?.startDate)}
|
||||||
onPress={() => toggleExpand(show.id)}
|
|
||||||
style={styles.moreChip}
|
|
||||||
>
|
|
||||||
<Text style={styles.moreChipText}>
|
|
||||||
+{restCount} mehr
|
|
||||||
</Text>
|
</Text>
|
||||||
|
</View>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.cardSearchButton}
|
||||||
|
onPress={() =>
|
||||||
|
WebBrowser.openBrowserAsync(
|
||||||
|
"https://www.google.com/search?udm=2&q=" +
|
||||||
|
encodeURIComponent(
|
||||||
|
`${String(name)} ${show.title}`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name="images-outline"
|
||||||
|
size={16}
|
||||||
|
color="#199edb"
|
||||||
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{partners.length > 0 && (
|
||||||
|
<View style={styles.partnerSection}>
|
||||||
|
<Text style={styles.sectionLabel}>Partner</Text>
|
||||||
|
<View style={styles.partnerRow}>
|
||||||
|
{partners.map((p) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={p.personId}
|
||||||
|
style={styles.partnerChip}
|
||||||
|
onPress={() => goToPerson(p)}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name="heart"
|
||||||
|
size={12}
|
||||||
|
color="#e74c8b"
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={styles.partnerChipText}
|
||||||
|
numberOfLines={1}
|
||||||
|
>
|
||||||
|
{p.name}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isExpanded && allParticipants.length > 12 && (
|
{allParticipants.length > 0 && (
|
||||||
<TouchableOpacity
|
<View style={styles.participantsSection}>
|
||||||
onPress={() => toggleExpand(show.id)}
|
<Text style={styles.sectionLabel}>
|
||||||
style={styles.moreChip}
|
Weitere Teilnehmer
|
||||||
>
|
</Text>
|
||||||
<Text style={styles.moreChipText}>Weniger</Text>
|
<View style={styles.participantRow}>
|
||||||
</TouchableOpacity>
|
{visible.map((p) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={p.personId}
|
||||||
|
style={styles.participantChip}
|
||||||
|
onPress={() => goToPerson(p)}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={styles.participantChipText}
|
||||||
|
numberOfLines={1}
|
||||||
|
>
|
||||||
|
{p.name}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{!isExpanded && restCount > 0 && (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => toggleExpand(show.id)}
|
||||||
|
style={styles.moreChip}
|
||||||
|
>
|
||||||
|
<Text style={styles.moreChipText}>
|
||||||
|
+{restCount} mehr
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isExpanded && allParticipants.length > 12 && (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => toggleExpand(show.id)}
|
||||||
|
style={styles.moreChip}
|
||||||
|
>
|
||||||
|
<Text style={styles.moreChipText}>Weniger</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
);
|
||||||
{partners.length > 0 && (
|
})}
|
||||||
<>
|
</ScrollView>
|
||||||
<View style={styles.horizontalLine} />
|
</View>
|
||||||
<Text
|
)}
|
||||||
style={[styles.participantLabel, { marginTop: 10 }]}
|
|
||||||
>
|
|
||||||
Partner
|
|
||||||
</Text>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.showContainer,
|
|
||||||
{
|
|
||||||
backgroundColor: "hsl(221, 39%, 12%)",
|
|
||||||
width: 150,
|
|
||||||
marginTop: 20,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
style={styles.showImage}
|
|
||||||
blurRadius={20}
|
|
||||||
source={{
|
|
||||||
uri: `https://i.pravatar.cc/300?img=${Math.floor(Math.random() * 70)}`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
{partners.map((p) => (
|
|
||||||
<Text
|
|
||||||
key={p.personId}
|
|
||||||
style={styles.partnerLabel}
|
|
||||||
numberOfLines={1}
|
|
||||||
>
|
|
||||||
{p.name}
|
|
||||||
</Text>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ScrollView>
|
|
||||||
</View>
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</GestureHandlerRootView>
|
</GestureHandlerRootView>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import ParticipantDetails from "@/components/ui/ParticipantDeatails";
|
import ParticipantDetails from "@/components/ui/ParticipantDeatails";
|
||||||
import ShowInfo from "@/components/ui/ShowInfo";
|
import ShowInfo from "@/components/ui/ShowInfo";
|
||||||
import StackHeader from "@/components/ui/StackHeader";
|
|
||||||
import {
|
import {
|
||||||
useSeasonCount,
|
useSeasonCount,
|
||||||
useSeasonDates,
|
useSeasonDates,
|
||||||
@@ -11,9 +10,11 @@ import * as Haptics from "expo-haptics";
|
|||||||
import { router, useLocalSearchParams } from "expo-router";
|
import { router, useLocalSearchParams } from "expo-router";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
Dimensions,
|
Dimensions,
|
||||||
Image,
|
Image,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
@@ -21,15 +22,17 @@ import {
|
|||||||
import styles from "./stackStyles/showDetailStyles";
|
import styles from "./stackStyles/showDetailStyles";
|
||||||
|
|
||||||
export default function ShowDetails() {
|
export default function ShowDetails() {
|
||||||
const { id } = useLocalSearchParams();
|
const { id, logoUri } = useLocalSearchParams();
|
||||||
const showId = Number(id);
|
const showId = Number(id);
|
||||||
|
const logoUriString = Array.isArray(logoUri) ? logoUri[0] : logoUri;
|
||||||
|
|
||||||
const [selectedParticipants, setSelectedParticipants] =
|
const [selectedParticipants, setSelectedParticipants] =
|
||||||
React.useState<boolean>(true);
|
React.useState<boolean>(true);
|
||||||
const [selectedSeason, setSelectedSeason] = React.useState<number>(1);
|
const [selectedSeason, setSelectedSeason] = React.useState<number>(1);
|
||||||
|
|
||||||
const { data: show } = useShow(showId);
|
const { data: show, isLoading: showLoading } = useShow(showId);
|
||||||
const { data: seasonCount = 0 } = useSeasonCount(showId);
|
const { data: seasonCount = 0, isLoading: seasonCountLoading } =
|
||||||
|
useSeasonCount(showId);
|
||||||
const {
|
const {
|
||||||
data: participants,
|
data: participants,
|
||||||
isLoading: pLoading,
|
isLoading: pLoading,
|
||||||
@@ -40,7 +43,7 @@ export default function ShowDetails() {
|
|||||||
|
|
||||||
const sortedParticipants = React.useMemo(() => {
|
const sortedParticipants = React.useMemo(() => {
|
||||||
return [...participants].sort((a, b) =>
|
return [...participants].sort((a, b) =>
|
||||||
a.name.localeCompare(b.name, "de", { sensitivity: "base" })
|
a.name.localeCompare(b.name, "de", { sensitivity: "base" }),
|
||||||
);
|
);
|
||||||
}, [participants]);
|
}, [participants]);
|
||||||
|
|
||||||
@@ -62,156 +65,227 @@ export default function ShowDetails() {
|
|||||||
}, [startDate]);
|
}, [startDate]);
|
||||||
|
|
||||||
const handleOpenParticipant = React.useCallback(
|
const handleOpenParticipant = React.useCallback(
|
||||||
(p: { id: number; name: string }) => {
|
(p: { id: number; name: string; imageUri?: string }) => {
|
||||||
router.push({
|
router.push({
|
||||||
pathname: "/participant",
|
pathname: "/participant",
|
||||||
params: {
|
params: {
|
||||||
participantId: p.id,
|
participantId: p.id,
|
||||||
name: p.name,
|
name: p.name,
|
||||||
|
imageUri: p.imageUri || "",
|
||||||
originShowId: String(showId),
|
originShowId: String(showId),
|
||||||
originSeason: String(selectedSeason),
|
originSeason: String(selectedSeason),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[showId, selectedSeason]
|
[showId, selectedSeason],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isInitialLoading = showLoading || seasonCountLoading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.mainContainer}>
|
<View style={styles.mainContainer}>
|
||||||
<StackHeader />
|
{isInitialLoading ? (
|
||||||
<ScrollView
|
<View style={styles.loadingContainer}>
|
||||||
showsVerticalScrollIndicator={false}
|
<ActivityIndicator size="large" color="#199edb" />
|
||||||
contentContainerStyle={{
|
|
||||||
paddingBottom: Dimensions.get("window").height * 0.1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{formattedStartDate ? (
|
|
||||||
<Text style={styles.startDate}>{formattedStartDate}</Text>
|
|
||||||
) : null}
|
|
||||||
<ShowInfo
|
|
||||||
seasons={seasonCount}
|
|
||||||
participants={participants.length}
|
|
||||||
streamingService={show?.streamingService as string}
|
|
||||||
startDate={startDate as string}
|
|
||||||
endDate={show?.endDate as string | null}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<View style={styles.showBannerLogoContainer}>
|
|
||||||
<Image
|
|
||||||
source={{
|
|
||||||
uri: show?.bannerUri as string,
|
|
||||||
}}
|
|
||||||
style={styles.showBannerLogo}
|
|
||||||
resizeMode="cover"
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.infoContainner}>
|
) : (
|
||||||
<TouchableOpacity onPress={() => setSelectedParticipants(true)}>
|
<ScrollView
|
||||||
<Text
|
showsVerticalScrollIndicator={false}
|
||||||
style={[
|
contentContainerStyle={{
|
||||||
styles.infoLabel,
|
paddingBottom: Dimensions.get("window").height * 0.1,
|
||||||
{
|
}}
|
||||||
fontWeight: selectedParticipants ? "bold" : "normal",
|
>
|
||||||
color: selectedParticipants ? "#199edb" : "hsl(0, 0%, 65%)",
|
{logoUriString ? (
|
||||||
},
|
<View style={styles.logoContainer}>
|
||||||
]}
|
<Image
|
||||||
>
|
source={{ uri: logoUriString }}
|
||||||
Teilnehmer
|
style={styles.showLogo}
|
||||||
</Text>
|
resizeMode="contain"
|
||||||
</TouchableOpacity>
|
/>
|
||||||
<TouchableOpacity onPress={() => setSelectedParticipants(false)}>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
styles.infoLabel,
|
|
||||||
{
|
|
||||||
fontWeight: !selectedParticipants ? "bold" : "normal",
|
|
||||||
color: !selectedParticipants ? "#199edb" : "hsl(0, 0%, 65%)",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
Details
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
{selectedParticipants ? (
|
|
||||||
<>
|
|
||||||
<View style={styles.seasonsSection}>
|
|
||||||
<Text style={styles.seasonsLabel}>Staffeln</Text>
|
|
||||||
<ScrollView
|
|
||||||
horizontal
|
|
||||||
showsHorizontalScrollIndicator={false}
|
|
||||||
contentContainerStyle={styles.seasonList}
|
|
||||||
>
|
|
||||||
{Array.from({ length: seasonCount }, (_, idx) => idx + 1).map(
|
|
||||||
(season) => (
|
|
||||||
<TouchableOpacity
|
|
||||||
key={season}
|
|
||||||
style={[
|
|
||||||
styles.seasonContainer,
|
|
||||||
{
|
|
||||||
backgroundColor:
|
|
||||||
selectedSeason === season
|
|
||||||
? "#199edb"
|
|
||||||
: "hsl(0, 0%, 20%)",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onPress={() => {
|
|
||||||
setSelectedSeason(season);
|
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text style={styles.seasonLabel}>{season}</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</ScrollView>
|
|
||||||
</View>
|
</View>
|
||||||
|
) : null}
|
||||||
<View
|
{formattedStartDate ? (
|
||||||
style={[
|
<Text style={styles.startDate}>{formattedStartDate}</Text>
|
||||||
styles.participantsDetailsContainer,
|
) : null}
|
||||||
styles.participantSection,
|
<ShowInfo
|
||||||
]}
|
seasons={seasonCount}
|
||||||
>
|
participants={participants.length}
|
||||||
{pError && (
|
|
||||||
<Text style={{ color: "tomato", marginBottom: 8 }}>
|
|
||||||
{pError}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{!pLoading && !pError && participants.length === 0 && (
|
|
||||||
<Text style={{ color: "gray" }}>Keine Teilnehmer.</Text>
|
|
||||||
)}
|
|
||||||
{sortedParticipants.map((p) => (
|
|
||||||
<TouchableOpacity
|
|
||||||
key={p.id}
|
|
||||||
style={[
|
|
||||||
styles.participantContainer,
|
|
||||||
{ backgroundColor: "hsl(336, 79%, 63%)" },
|
|
||||||
]}
|
|
||||||
onPress={() => handleOpenParticipant(p)}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
source={{ uri: p.imageUri }}
|
|
||||||
style={{ width: "100%", height: "100%", borderRadius: 10 }}
|
|
||||||
resizeMode="cover"
|
|
||||||
blurRadius={p.imageUri.includes("pravatar") ? 16 : 0}
|
|
||||||
/>
|
|
||||||
<Text style={styles.participantLabel} numberOfLines={2}>
|
|
||||||
{p.name}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<ParticipantDetails
|
|
||||||
description={show?.description as string}
|
|
||||||
concept={show?.concept as string}
|
|
||||||
genres={show?.genres as string[]}
|
|
||||||
streamingService={show?.streamingService as string}
|
streamingService={show?.streamingService as string}
|
||||||
|
startDate={startDate as string}
|
||||||
|
endDate={show?.endDate as string | null}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</ScrollView>
|
<View style={styles.showBannerLogoContainer}>
|
||||||
|
<Image
|
||||||
|
source={{
|
||||||
|
uri: show?.bannerUri as string,
|
||||||
|
}}
|
||||||
|
style={styles.showBannerLogo}
|
||||||
|
resizeMode="cover"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={styles.infoContainner}>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => 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)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.infoLabel,
|
||||||
|
{
|
||||||
|
fontWeight: selectedParticipants ? "700" : "500",
|
||||||
|
color: selectedParticipants
|
||||||
|
? "#199edb"
|
||||||
|
: "rgba(255,255,255,0.45)",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
Teilnehmer
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => 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)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.infoLabel,
|
||||||
|
{
|
||||||
|
fontWeight: !selectedParticipants ? "700" : "500",
|
||||||
|
color: !selectedParticipants
|
||||||
|
? "#199edb"
|
||||||
|
: "rgba(255,255,255,0.45)",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
Details
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
{selectedParticipants ? (
|
||||||
|
<>
|
||||||
|
<View style={styles.seasonsSection}>
|
||||||
|
<Text style={styles.seasonsLabel}>Staffeln</Text>
|
||||||
|
<ScrollView
|
||||||
|
horizontal
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
contentContainerStyle={styles.seasonList}
|
||||||
|
>
|
||||||
|
{Array.from({ length: seasonCount }, (_, idx) => idx + 1).map(
|
||||||
|
(season) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={season}
|
||||||
|
style={[
|
||||||
|
styles.seasonContainer,
|
||||||
|
{
|
||||||
|
backgroundColor:
|
||||||
|
selectedSeason === season
|
||||||
|
? "#199edb"
|
||||||
|
: "rgba(255,255,255,0.08)",
|
||||||
|
borderColor:
|
||||||
|
selectedSeason === season
|
||||||
|
? "rgba(25,158,219,0.3)"
|
||||||
|
: "rgba(255,255,255,0.06)",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onPress={() => {
|
||||||
|
setSelectedSeason(season);
|
||||||
|
Haptics.impactAsync(
|
||||||
|
Haptics.ImpactFeedbackStyle.Light,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={styles.seasonLabel}>{season}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.participantsDetailsContainer,
|
||||||
|
styles.participantSection,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{pError && (
|
||||||
|
<Text
|
||||||
|
style={{ color: "tomato", marginBottom: 8, fontSize: 13 }}
|
||||||
|
>
|
||||||
|
{pError}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{pLoading && (
|
||||||
|
<View style={styles.sectionLoading}>
|
||||||
|
<ActivityIndicator size="small" color="#199edb" />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
{!pLoading && !pError && participants.length === 0 && (
|
||||||
|
<Text
|
||||||
|
style={{ color: "rgba(255,255,255,0.4)", fontSize: 14 }}
|
||||||
|
>
|
||||||
|
Keine Teilnehmer.
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{!pLoading &&
|
||||||
|
sortedParticipants.map((p) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={p.id}
|
||||||
|
style={styles.participantWrapper}
|
||||||
|
onPress={() => handleOpenParticipant(p)}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.participantContainer,
|
||||||
|
{ backgroundColor: "hsl(336, 79%, 63%)" },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
source={{ uri: p.imageUri }}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
borderRadius: 16,
|
||||||
|
}}
|
||||||
|
resizeMode="cover"
|
||||||
|
blurRadius={p.imageUri.includes("pravatar") ? 16 : 0}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.participantLabel} numberOfLines={2}>
|
||||||
|
{p.name}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<ParticipantDetails
|
||||||
|
description={show?.description as string}
|
||||||
|
concept={show?.concept as string}
|
||||||
|
genres={show?.genres as string[]}
|
||||||
|
streamingService={show?.streamingService as string}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,49 @@
|
|||||||
import { Dimensions, StyleSheet } from "react-native";
|
|
||||||
import { Colors } from "@/constants/colors";
|
import { Colors } from "@/constants/colors";
|
||||||
|
import { Dimensions, StyleSheet } from "react-native";
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
mainContainer: {
|
mainContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: Colors.header,
|
backgroundColor: Colors.background,
|
||||||
|
paddingTop: 20,
|
||||||
},
|
},
|
||||||
closeIcon: {
|
profileHero: {
|
||||||
position: "absolute",
|
alignItems: "center",
|
||||||
top: Dimensions.get("window").height * 0.065,
|
paddingTop: 8,
|
||||||
right: 15,
|
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: {
|
participantName: {
|
||||||
color: Colors.text,
|
color: Colors.text,
|
||||||
fontSize: 20,
|
fontSize: 24,
|
||||||
fontWeight: "600",
|
fontWeight: "700",
|
||||||
textAlign: "center",
|
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: {
|
participantImage: {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
@@ -36,36 +63,34 @@ const styles = StyleSheet.create({
|
|||||||
marginTop: 5,
|
marginTop: 5,
|
||||||
},
|
},
|
||||||
participantInfo: {
|
participantInfo: {
|
||||||
color: Colors.textSecondary,
|
color: "rgba(255,255,255,0.6)",
|
||||||
fontSize: 16,
|
fontSize: 15,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
},
|
},
|
||||||
dot: {
|
dot: {
|
||||||
width: 4,
|
width: 4,
|
||||||
height: 4,
|
height: 4,
|
||||||
borderRadius: 3,
|
borderRadius: 2,
|
||||||
backgroundColor: Colors.textSecondary,
|
backgroundColor: "rgba(255,255,255,0.3)",
|
||||||
marginHorizontal: 7,
|
marginHorizontal: 7,
|
||||||
marginTop: 2,
|
|
||||||
},
|
},
|
||||||
performedShowsSection: {
|
performedShowsSection: {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
backgroundColor: "transparent",
|
||||||
backgroundColor: Colors.background,
|
paddingBottom: 40,
|
||||||
marginTop: 20,
|
|
||||||
},
|
},
|
||||||
performedShowsTitle: {
|
performedShowsTitle: {
|
||||||
fontSize: 16,
|
fontSize: 18,
|
||||||
fontWeight: "600",
|
fontWeight: "700",
|
||||||
color: Colors.textSecondary,
|
color: Colors.text,
|
||||||
marginTop: 15,
|
marginTop: 8,
|
||||||
marginLeft: 15,
|
marginLeft: 16,
|
||||||
|
marginBottom: 4,
|
||||||
|
letterSpacing: 0.2,
|
||||||
},
|
},
|
||||||
|
|
||||||
showImage: {
|
showImage: {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
borderRadius: 10,
|
|
||||||
},
|
},
|
||||||
showLabel: {
|
showLabel: {
|
||||||
color: Colors.text,
|
color: Colors.text,
|
||||||
@@ -85,102 +110,165 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
showTitle: {
|
showTitle: {
|
||||||
color: Colors.text,
|
color: Colors.text,
|
||||||
fontSize: 12,
|
fontSize: 16,
|
||||||
fontWeight: "600",
|
fontWeight: "700",
|
||||||
textAlign: "center",
|
letterSpacing: 0.1,
|
||||||
marginTop: 15,
|
|
||||||
},
|
},
|
||||||
showSeason: {
|
showSeason: {
|
||||||
color: Colors.textSecondary,
|
color: "rgba(255,255,255,0.45)",
|
||||||
fontSize: 12,
|
fontSize: 13,
|
||||||
fontWeight: "400",
|
fontWeight: "500",
|
||||||
textAlign: "center",
|
|
||||||
marginTop: 5,
|
|
||||||
},
|
},
|
||||||
showContainer: {
|
showContainer: {
|
||||||
width: Dimensions.get("window").width - 75,
|
width: Dimensions.get("window").width - 64,
|
||||||
height: 200,
|
height: 180,
|
||||||
borderRadius: 15,
|
borderTopLeftRadius: 20,
|
||||||
marginTop: 20,
|
borderTopRightRadius: 20,
|
||||||
alignItems: "center",
|
overflow: "hidden",
|
||||||
backgroundColor: Colors.primary,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
card: {
|
card: {
|
||||||
width: Dimensions.get("window").width - 75,
|
width: Dimensions.get("window").width - 64,
|
||||||
alignItems: "center",
|
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,
|
||||||
},
|
},
|
||||||
|
cardInfo: {
|
||||||
horizontalLine: {
|
padding: 16,
|
||||||
height: 50,
|
gap: 4,
|
||||||
width: 2,
|
|
||||||
backgroundColor: Colors.textSecondary,
|
|
||||||
marginTop: 10,
|
|
||||||
alignSelf: "center",
|
|
||||||
},
|
},
|
||||||
partnerLabel: {
|
cardTitleRow: {
|
||||||
color: Colors.textSecondary,
|
flexDirection: "row",
|
||||||
fontSize: 12,
|
alignItems: "flex-start",
|
||||||
fontWeight: "400",
|
gap: 12,
|
||||||
textAlign: "center",
|
|
||||||
marginTop: 10,
|
|
||||||
},
|
},
|
||||||
participantContainer: {
|
cardSearchButton: {
|
||||||
width: "auto",
|
width: 34,
|
||||||
minHeight: "auto",
|
height: 34,
|
||||||
borderRadius: 15,
|
borderRadius: 17,
|
||||||
marginTop: 15,
|
backgroundColor: "rgba(25,158,219,0.15)",
|
||||||
alignItems: "center",
|
borderWidth: StyleSheet.hairlineWidth,
|
||||||
|
borderColor: "rgba(25,158,219,0.25)",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
backgroundColor: Colors.header,
|
alignItems: "center",
|
||||||
padding: 10,
|
marginTop: 2,
|
||||||
},
|
},
|
||||||
|
sectionLabel: {
|
||||||
participantLabel: {
|
color: "rgba(255,255,255,0.45)",
|
||||||
color: Colors.text,
|
fontSize: 11,
|
||||||
fontSize: 12,
|
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: {
|
participantRow: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
flexWrap: "wrap",
|
flexWrap: "wrap",
|
||||||
gap: 6,
|
gap: 6,
|
||||||
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "flex-start",
|
|
||||||
},
|
},
|
||||||
|
|
||||||
participantChip: {
|
participantChip: {
|
||||||
paddingVertical: 4,
|
paddingVertical: 5,
|
||||||
paddingHorizontal: 8,
|
paddingHorizontal: 10,
|
||||||
borderRadius: 12,
|
borderRadius: 14,
|
||||||
backgroundColor: "hsl(221, 39%, 18%)",
|
backgroundColor: "rgba(255,255,255,0.08)",
|
||||||
maxWidth: 160,
|
maxWidth: 160,
|
||||||
},
|
},
|
||||||
participantChipText: {
|
participantChipText: {
|
||||||
color: "hsl(0, 0%, 85%)",
|
color: "rgba(255,255,255,0.7)",
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
|
fontWeight: "500",
|
||||||
},
|
},
|
||||||
|
|
||||||
moreChip: {
|
moreChip: {
|
||||||
paddingVertical: 4,
|
paddingVertical: 5,
|
||||||
paddingHorizontal: 10,
|
paddingHorizontal: 12,
|
||||||
borderRadius: 12,
|
borderRadius: 16,
|
||||||
backgroundColor: "hsl(221, 39%, 28%)",
|
backgroundColor: "rgba(25,158,219,0.2)",
|
||||||
|
borderWidth: StyleSheet.hairlineWidth,
|
||||||
|
borderColor: "rgba(25,158,219,0.3)",
|
||||||
},
|
},
|
||||||
moreChipText: {
|
moreChipText: {
|
||||||
color: Colors.text,
|
color: "#199edb",
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontWeight: "600",
|
fontWeight: "600",
|
||||||
},
|
},
|
||||||
|
heroButtons: {
|
||||||
|
flexDirection: "row",
|
||||||
|
gap: 10,
|
||||||
|
marginTop: 16,
|
||||||
|
},
|
||||||
searchButton: {
|
searchButton: {
|
||||||
width: 50,
|
flexDirection: "row",
|
||||||
height: 50,
|
alignItems: "center",
|
||||||
borderRadius: 20,
|
gap: 8,
|
||||||
backgroundColor: Colors.header,
|
paddingVertical: 10,
|
||||||
marginLeft: 15,
|
paddingHorizontal: 18,
|
||||||
marginTop: 15,
|
borderRadius: 22,
|
||||||
marginBottom: 5,
|
backgroundColor: "rgba(25,158,219,0.15)",
|
||||||
justifyContent: "center",
|
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",
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
import { StyleSheet } from "react-native";
|
|
||||||
import { Colors } from "@/constants/colors";
|
import { Colors } from "@/constants/colors";
|
||||||
|
import { Dimensions, StyleSheet } from "react-native";
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
mainContainer: {
|
mainContainer: {
|
||||||
flex: 1,
|
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: {
|
showImage: {
|
||||||
width: 200,
|
width: 200,
|
||||||
@@ -14,136 +24,168 @@ const styles = StyleSheet.create({
|
|||||||
bottom: 10,
|
bottom: 10,
|
||||||
},
|
},
|
||||||
showMainInfoSection: {
|
showMainInfoSection: {
|
||||||
width: "auto",
|
|
||||||
height: "auto",
|
|
||||||
alignSelf: "center",
|
alignSelf: "center",
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
bottom: 25,
|
gap: 8,
|
||||||
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
showInfoText: {
|
showInfoText: {
|
||||||
color: Colors.textSecondary,
|
color: "rgba(255,255,255,0.7)",
|
||||||
fontSize: 14,
|
fontSize: 13,
|
||||||
|
fontWeight: "500",
|
||||||
},
|
},
|
||||||
dot: {
|
dot: {
|
||||||
width: 4,
|
width: 4,
|
||||||
height: 4,
|
height: 4,
|
||||||
borderRadius: 3,
|
borderRadius: 2,
|
||||||
backgroundColor: Colors.textSecondary,
|
backgroundColor: "rgba(255,255,255,0.3)",
|
||||||
marginHorizontal: 7,
|
marginHorizontal: 6,
|
||||||
marginTop: 2,
|
|
||||||
},
|
},
|
||||||
showBannerLogoContainer: {
|
showBannerLogoContainer: {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: 200,
|
height: 220,
|
||||||
alignSelf: "center",
|
alignSelf: "center",
|
||||||
borderTopLeftRadius: 80,
|
marginTop: 8,
|
||||||
borderTopRightRadius: 80,
|
|
||||||
marginTop: 15,
|
|
||||||
},
|
},
|
||||||
showBannerLogo: {
|
showBannerLogo: {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
borderTopLeftRadius: 30,
|
borderTopLeftRadius: 28,
|
||||||
borderTopRightRadius: 30,
|
borderTopRightRadius: 28,
|
||||||
},
|
},
|
||||||
infoContainner: {
|
infoContainner: {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
minHeight: "auto",
|
minHeight: "auto",
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 20,
|
||||||
paddingVertical: 15,
|
paddingVertical: 14,
|
||||||
backgroundColor: Colors.background,
|
backgroundColor: "transparent",
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
gap: 20,
|
gap: 6,
|
||||||
},
|
},
|
||||||
infoLabel: {
|
infoLabel: {
|
||||||
fontWeight: "300",
|
fontWeight: "500",
|
||||||
color: Colors.textSecondary,
|
color: "rgba(255,255,255,0.5)",
|
||||||
fontSize: 16,
|
fontSize: 15,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 8,
|
||||||
|
borderRadius: 20,
|
||||||
|
overflow: "hidden",
|
||||||
|
letterSpacing: 0.3,
|
||||||
},
|
},
|
||||||
participantsDetailsContainer: {
|
participantsDetailsContainer: {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
minHeight: 200,
|
||||||
backgroundColor: Colors.card,
|
backgroundColor: "transparent",
|
||||||
|
},
|
||||||
|
participantWrapper: {
|
||||||
|
width: (Dimensions.get("window").width - 32 - 24) / 3,
|
||||||
|
alignItems: "center",
|
||||||
|
marginBottom: 16,
|
||||||
},
|
},
|
||||||
participantContainer: {
|
participantContainer: {
|
||||||
height: 160,
|
width: "100%",
|
||||||
width: 110,
|
aspectRatio: 0.72,
|
||||||
backgroundColor: Colors.primary,
|
borderRadius: 16,
|
||||||
borderRadius: 10,
|
overflow: "hidden",
|
||||||
marginBottom: 30,
|
shadowColor: "#000",
|
||||||
|
shadowOffset: { width: 0, height: 4 },
|
||||||
|
shadowOpacity: 0.2,
|
||||||
|
shadowRadius: 8,
|
||||||
|
elevation: 4,
|
||||||
},
|
},
|
||||||
participantSection: {
|
participantSection: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
flexWrap: "wrap",
|
flexWrap: "wrap",
|
||||||
gap: 15,
|
gap: 12,
|
||||||
paddingLeft: 15,
|
paddingHorizontal: 16,
|
||||||
paddingTop: 15,
|
paddingTop: 12,
|
||||||
},
|
},
|
||||||
seasonsSection: {
|
seasonsSection: {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
minHeight: 40,
|
minHeight: 50,
|
||||||
backgroundColor: Colors.card,
|
backgroundColor: "transparent",
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 10,
|
gap: 12,
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 20,
|
||||||
|
paddingVertical: 8,
|
||||||
},
|
},
|
||||||
seasonList: {
|
seasonList: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 10,
|
gap: 8,
|
||||||
paddingLeft: 5,
|
paddingLeft: 4,
|
||||||
paddingRight: 5,
|
paddingRight: 8,
|
||||||
},
|
},
|
||||||
seasonContainer: {
|
seasonContainer: {
|
||||||
width: 35,
|
width: 40,
|
||||||
height: 35,
|
height: 40,
|
||||||
borderRadius: 5,
|
borderRadius: 20,
|
||||||
backgroundColor: "hsl(0, 0%, 20%)",
|
backgroundColor: "rgba(255,255,255,0.08)",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
borderWidth: StyleSheet.hairlineWidth,
|
||||||
|
borderColor: "rgba(255,255,255,0.06)",
|
||||||
},
|
},
|
||||||
seasonLabel: {
|
seasonLabel: {
|
||||||
color: Colors.text,
|
color: Colors.text,
|
||||||
fontWeight: "bold",
|
fontWeight: "700",
|
||||||
|
fontSize: 14,
|
||||||
},
|
},
|
||||||
participantLabel: {
|
participantLabel: {
|
||||||
color: Colors.text,
|
color: Colors.text,
|
||||||
fontWeight: "500",
|
fontWeight: "600",
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
fontSize: 11,
|
fontSize: 12,
|
||||||
marginTop: 10,
|
marginTop: 6,
|
||||||
|
letterSpacing: 0.1,
|
||||||
|
width: "100%",
|
||||||
},
|
},
|
||||||
seasonsLabel: {
|
seasonsLabel: {
|
||||||
color: Colors.textSecondary,
|
color: "rgba(255,255,255,0.6)",
|
||||||
fontWeight: "500",
|
fontWeight: "600",
|
||||||
fontSize: 16,
|
fontSize: 15,
|
||||||
|
letterSpacing: 0.2,
|
||||||
},
|
},
|
||||||
detailTitle: {
|
detailTitle: {
|
||||||
color: Colors.text,
|
color: "rgba(255,255,255,0.95)",
|
||||||
fontSize: 14,
|
fontSize: 15,
|
||||||
fontWeight: "bold",
|
fontWeight: "700",
|
||||||
marginTop: 10,
|
marginTop: 10,
|
||||||
marginLeft: 20,
|
marginLeft: 20,
|
||||||
marginBottom: 5,
|
marginBottom: 5,
|
||||||
|
letterSpacing: 0.2,
|
||||||
},
|
},
|
||||||
detailLabel: {
|
detailLabel: {
|
||||||
color: Colors.textSecondary,
|
color: "rgba(255,255,255,0.6)",
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
lineHeight: 20,
|
lineHeight: 22,
|
||||||
width: "90%",
|
width: "90%",
|
||||||
fontWeight: "300",
|
fontWeight: "400",
|
||||||
marginLeft: 20,
|
marginLeft: 20,
|
||||||
marginTop: 5,
|
marginTop: 5,
|
||||||
},
|
},
|
||||||
startDate: {
|
startDate: {
|
||||||
color: Colors.textSecondary,
|
color: "rgba(255,255,255,0.5)",
|
||||||
fontSize: 16,
|
fontSize: 14,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
marginTop: 15,
|
marginTop: 14,
|
||||||
fontStyle: "italic",
|
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;
|
export default styles;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export default StyleSheet.create({
|
|||||||
mainContainer: {
|
mainContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: Colors.background,
|
backgroundColor: Colors.background,
|
||||||
paddingHorizontal: 5,
|
paddingHorizontal: 10,
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
minHeight: 125,
|
minHeight: 125,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { FontAwesome } from "@expo/vector-icons";
|
import Feather from "@expo/vector-icons/Feather";
|
||||||
import { router } from "expo-router";
|
import { router } from "expo-router";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
import { Image, StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
||||||
|
|
||||||
export type PersonLite = {
|
export type PersonLite = {
|
||||||
id?: number;
|
id?: number;
|
||||||
@@ -24,66 +24,110 @@ const calcAge = (birthDate?: string | null): number | null => {
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
person: any;
|
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 age = calcAge(person.birthDate);
|
||||||
const id = person.personId ?? person.id;
|
const id = person.personId ?? person.id;
|
||||||
|
const imageUrl = person.imageUrl ?? person.imageUri ?? null;
|
||||||
|
const isPravatar = imageUrl?.includes("pravatar");
|
||||||
|
|
||||||
const goToPerson = React.useCallback(
|
const goToPerson = React.useCallback(
|
||||||
(id: number) => {
|
(id: number) => {
|
||||||
console.log("go to person", id);
|
|
||||||
router.push({
|
router.push({
|
||||||
pathname: "/participant",
|
pathname: "/participant",
|
||||||
params: { participantId: String(id), name: person.name },
|
params: {
|
||||||
|
participantId: String(id),
|
||||||
|
name: person.name,
|
||||||
|
imageUri: imageUrl || "",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[person.name]
|
[person.name, imageUrl],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => {
|
onPress={() => goToPerson(Number(id))}
|
||||||
goToPerson(Number(id));
|
style={[
|
||||||
}}
|
styles.personRow,
|
||||||
style={styles.personRow}
|
isFirst && styles.firstRow,
|
||||||
|
isLast && styles.lastRow,
|
||||||
|
]}
|
||||||
|
activeOpacity={0.6}
|
||||||
>
|
>
|
||||||
<View style={styles.avatarCircle}>
|
<View style={styles.avatarCircle}>
|
||||||
<FontAwesome name="user" size={22} color="#ccc" />
|
{imageUrl && !isPravatar ? (
|
||||||
|
<Image
|
||||||
|
source={{ uri: imageUrl }}
|
||||||
|
style={styles.avatarImage}
|
||||||
|
resizeMode="cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Feather name="user" size={20} color="rgba(255,255,255,0.7)" />
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
<View style={{ flex: 1 }}>
|
<View style={styles.content}>
|
||||||
<Text style={styles.personName}>
|
<View style={{ flex: 1 }}>
|
||||||
{person.name || "Unbekannt"}
|
<Text style={styles.personName}>{person.name || "Unbekannt"}</Text>
|
||||||
{age != null ? ` (${age})` : ""}
|
{age != null && <Text style={styles.personMeta}>{age} Jahre</Text>}
|
||||||
</Text>
|
</View>
|
||||||
{/* <Text style={styles.personMeta}>aus: unterschiedlichen Shows</Text> */}
|
<Feather name="chevron-right" size={16} color="rgba(255,255,255,0.3)" />
|
||||||
</View>
|
</View>
|
||||||
<FontAwesome name="chevron-right" size={14} color="#888" />
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
personRow: {
|
personRow: {
|
||||||
width: "100%",
|
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
backgroundColor: "#1b1e2b",
|
backgroundColor: "rgba(255,255,255,0.06)",
|
||||||
borderRadius: 10,
|
paddingLeft: 16,
|
||||||
paddingHorizontal: 10,
|
minHeight: 56,
|
||||||
paddingVertical: 10,
|
},
|
||||||
marginBottom: 8,
|
firstRow: {
|
||||||
|
borderTopLeftRadius: 10,
|
||||||
|
borderTopRightRadius: 10,
|
||||||
|
},
|
||||||
|
lastRow: {
|
||||||
|
borderBottomLeftRadius: 10,
|
||||||
|
borderBottomRightRadius: 10,
|
||||||
},
|
},
|
||||||
avatarCircle: {
|
avatarCircle: {
|
||||||
width: 40,
|
width: 36,
|
||||||
height: 40,
|
height: 36,
|
||||||
borderRadius: 999,
|
borderRadius: 18,
|
||||||
backgroundColor: "#2a2f45",
|
backgroundColor: "rgba(255,255,255,0.1)",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "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 },
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,20 +1,45 @@
|
|||||||
import { FontAwesome } from "@expo/vector-icons";
|
import Feather from "@expo/vector-icons/Feather";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
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 (
|
return (
|
||||||
<TouchableOpacity onPress={onPress}>
|
<TouchableOpacity onPress={onPress} activeOpacity={0.7}>
|
||||||
<View style={styles.tag}>
|
<View style={styles.tag}>
|
||||||
<FontAwesome name={icon} size={16} color="#bbb" style={{ marginRight: 6 }} />
|
|
||||||
<Text style={styles.tagLabel}>{label}</Text>
|
<Text style={styles.tagLabel}>{label}</Text>
|
||||||
<FontAwesome name="times-circle" size={16} color="#bbb" style={{ marginLeft: 6 }} />
|
<Feather
|
||||||
|
name="x"
|
||||||
|
size={14}
|
||||||
|
color="rgba(255,255,255,0.5)"
|
||||||
|
style={{ marginLeft: 4 }}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
tag: { flexDirection: "row", alignItems: "center", backgroundColor: "#333", borderRadius: 999, paddingHorizontal: 10, paddingVertical: 6, marginRight: 8, marginBottom: 8 },
|
tag: {
|
||||||
tagLabel: { color: "white" },
|
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",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { BlurView } from "expo-blur";
|
||||||
import { StyleSheet, Text, View } from "react-native";
|
import { StyleSheet, Text, View } from "react-native";
|
||||||
|
|
||||||
type ParticipantDetailsProps = {
|
type ParticipantDetailsProps = {
|
||||||
@@ -14,41 +15,77 @@ const ParticipantDetails = ({
|
|||||||
streamingService,
|
streamingService,
|
||||||
}: ParticipantDetailsProps) => {
|
}: ParticipantDetailsProps) => {
|
||||||
return (
|
return (
|
||||||
<View style={styles.participantsDetailsContainer}>
|
<View style={styles.container}>
|
||||||
<Text style={styles.detailTitle}>Beschreibung:</Text>
|
<BlurView intensity={20} tint="dark" style={styles.card}>
|
||||||
<Text style={styles.detailLabel}>{description}</Text>
|
<Text style={styles.detailTitle}>Beschreibung</Text>
|
||||||
<Text style={styles.detailTitle}>Konzept:</Text>
|
<Text style={styles.detailLabel}>{description}</Text>
|
||||||
<Text style={styles.detailLabel}>{concept}</Text>
|
</BlurView>
|
||||||
<Text style={styles.detailTitle}>Genres:</Text>
|
<BlurView intensity={20} tint="dark" style={styles.card}>
|
||||||
<Text style={styles.detailLabel}>{genres.join(', ')}</Text>
|
<Text style={styles.detailTitle}>Konzept</Text>
|
||||||
<Text style={styles.detailTitle}>Produktion:</Text>
|
<Text style={styles.detailLabel}>{concept}</Text>
|
||||||
<Text style={styles.detailLabel}>{streamingService}</Text>
|
</BlurView>
|
||||||
|
<BlurView intensity={20} tint="dark" style={styles.card}>
|
||||||
|
<Text style={styles.detailTitle}>Genres</Text>
|
||||||
|
<View style={styles.genreRow}>
|
||||||
|
{genres.map((g) => (
|
||||||
|
<View key={g} style={styles.genrePill}>
|
||||||
|
<Text style={styles.genrePillText}>{g}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</BlurView>
|
||||||
|
<BlurView intensity={20} tint="dark" style={styles.card}>
|
||||||
|
<Text style={styles.detailTitle}>Produktion</Text>
|
||||||
|
<Text style={styles.detailLabel}>{streamingService}</Text>
|
||||||
|
</BlurView>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
participantsDetailsContainer: {
|
container: {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
paddingHorizontal: 16,
|
||||||
backgroundColor: "hsl(221, 39%, 2%)",
|
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: {
|
detailTitle: {
|
||||||
color: "hsl(0, 0%, 100%)",
|
color: "rgba(255,255,255,0.95)",
|
||||||
fontSize: 14,
|
fontSize: 15,
|
||||||
fontWeight: "bold",
|
fontWeight: "700",
|
||||||
marginTop: 10,
|
marginBottom: 8,
|
||||||
marginLeft: 20,
|
letterSpacing: 0.2,
|
||||||
marginBottom: 5,
|
|
||||||
},
|
},
|
||||||
detailLabel: {
|
detailLabel: {
|
||||||
color: "hsl(0, 0%, 80%)",
|
color: "rgba(255,255,255,0.65)",
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
lineHeight: 20,
|
lineHeight: 22,
|
||||||
width: "90%",
|
fontWeight: "400",
|
||||||
fontWeight: "300",
|
},
|
||||||
marginLeft: 20,
|
genreRow: {
|
||||||
marginTop: 5,
|
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",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ type ShowCardProps = {
|
|||||||
imageUri: string;
|
imageUri: string;
|
||||||
streamingServicesUris: string[];
|
streamingServicesUris: string[];
|
||||||
liveBadgeText?: string;
|
liveBadgeText?: string;
|
||||||
liveBadgeContainerStyle?: object;
|
|
||||||
genres: string[];
|
genres: string[];
|
||||||
title: string;
|
title: string;
|
||||||
onPress?: () => void;
|
onPress?: () => void;
|
||||||
@@ -15,139 +14,144 @@ const ShowCard = ({
|
|||||||
imageUri,
|
imageUri,
|
||||||
streamingServicesUris,
|
streamingServicesUris,
|
||||||
liveBadgeText,
|
liveBadgeText,
|
||||||
liveBadgeContainerStyle,
|
|
||||||
genres,
|
genres,
|
||||||
onPress,
|
onPress,
|
||||||
title,
|
title,
|
||||||
}: ShowCardProps) => {
|
}: ShowCardProps) => {
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity style={styles.card} activeOpacity={0.8} onPress={onPress}>
|
||||||
style={styles.showContainer}
|
|
||||||
activeOpacity={0.3}
|
|
||||||
onPress={onPress}
|
|
||||||
>
|
|
||||||
<Image
|
<Image
|
||||||
source={{
|
source={{ uri: imageUri }}
|
||||||
uri: imageUri,
|
style={[StyleSheet.absoluteFillObject, { borderRadius: 18 }]}
|
||||||
}}
|
|
||||||
style={[StyleSheet.absoluteFillObject, { borderRadius: 35 }]}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<View style={{ flexDirection: 'row', width: '100%', justifyContent: 'flex-end', padding: 10, gap: 5}}>
|
{/* Gradient-like overlay at bottom */}
|
||||||
{streamingServicesUris.length > 0 && streamingServicesUris.map((service) => (
|
<View style={styles.bottomGradient} />
|
||||||
<Image
|
|
||||||
key={service}
|
|
||||||
source={{
|
|
||||||
uri: service,
|
|
||||||
}}
|
|
||||||
style={{ height: 45, width: 45, resizeMode: 'contain', borderRadius: 100}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
))}
|
{/* Streaming service icons */}
|
||||||
|
<View style={styles.serviceRow}>
|
||||||
|
{streamingServicesUris.length > 0 &&
|
||||||
|
streamingServicesUris.map((service) => (
|
||||||
|
<Image
|
||||||
|
key={service}
|
||||||
|
source={{ uri: service }}
|
||||||
|
style={styles.serviceIcon}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Live badge */}
|
||||||
{liveBadgeText && (
|
{liveBadgeText && (
|
||||||
<View style={liveBadgeContainerStyle}>
|
<View style={styles.liveBadge}>
|
||||||
|
<View style={styles.liveDot} />
|
||||||
<Text style={styles.liveBadgeText}>{liveBadgeText}</Text>
|
<Text style={styles.liveBadgeText}>{liveBadgeText}</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<View style={styles.titleSection}>
|
{/* Bottom info */}
|
||||||
<Text
|
<View style={styles.bottomInfo}>
|
||||||
style={{
|
<Text style={styles.title} numberOfLines={1}>
|
||||||
color: "white",
|
|
||||||
fontWeight: "bold",
|
|
||||||
fontSize: 12,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
{genres.length > 0 && (
|
||||||
<View style={styles.genreSection}>
|
<View style={styles.genreRow}>
|
||||||
{genres.map((genre) => (
|
{genres.slice(0, 3).map((genre) => (
|
||||||
<Text key={genre} style={styles.genreLabel}>
|
<Text key={genre} style={styles.genreTag}>
|
||||||
{genre}
|
{genre}
|
||||||
</Text>
|
</Text>
|
||||||
))}
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
showContainer: {
|
card: {
|
||||||
width: "100%",
|
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",
|
backgroundColor: "transparent",
|
||||||
alignSelf: "center",
|
// A dark gradient from bottom for readability
|
||||||
borderRadius: 35,
|
// Using a semi-transparent overlay at bottom
|
||||||
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,
|
|
||||||
},
|
},
|
||||||
streamingServiceIcon: {
|
serviceRow: {
|
||||||
width: 45,
|
flexDirection: "row",
|
||||||
height: 45,
|
justifyContent: "flex-end",
|
||||||
marginLeft: "auto",
|
padding: 10,
|
||||||
marginRight: 15,
|
gap: 6,
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: "white",
|
|
||||||
borderRadius: 15,
|
|
||||||
marginTop: 15,
|
|
||||||
},
|
},
|
||||||
liveBadgeContainer: {
|
serviceIcon: {
|
||||||
|
height: 34,
|
||||||
|
width: 34,
|
||||||
|
borderRadius: 17,
|
||||||
|
resizeMode: "contain",
|
||||||
|
backgroundColor: "rgba(0,0,0,0.3)",
|
||||||
|
},
|
||||||
|
liveBadge: {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: 15,
|
top: 12,
|
||||||
left: 20,
|
left: 12,
|
||||||
backgroundColor: "red",
|
flexDirection: "row",
|
||||||
borderRadius: 10,
|
alignItems: "center",
|
||||||
paddingVertical: 5,
|
gap: 5,
|
||||||
|
backgroundColor: "rgba(0,0,0,0.55)",
|
||||||
|
paddingVertical: 4,
|
||||||
paddingHorizontal: 10,
|
paddingHorizontal: 10,
|
||||||
|
borderRadius: 12,
|
||||||
|
},
|
||||||
|
liveDot: {
|
||||||
|
width: 7,
|
||||||
|
height: 7,
|
||||||
|
borderRadius: 4,
|
||||||
|
backgroundColor: "#ff3b30",
|
||||||
},
|
},
|
||||||
liveBadgeText: {
|
liveBadgeText: {
|
||||||
color: "white",
|
color: "white",
|
||||||
fontWeight: "bold",
|
fontWeight: "700",
|
||||||
|
fontSize: 11,
|
||||||
|
letterSpacing: 0.5,
|
||||||
},
|
},
|
||||||
genreSection: {
|
bottomInfo: {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
bottom: 15,
|
bottom: 0,
|
||||||
left: 20,
|
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",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
gap: 6,
|
||||||
justifyContent: "space-evenly",
|
marginTop: 5,
|
||||||
gap: 5,
|
flexWrap: "wrap",
|
||||||
},
|
},
|
||||||
genreLabel: {
|
genreTag: {
|
||||||
color: "red",
|
color: "rgba(255,255,255,0.8)",
|
||||||
fontWeight: "bold",
|
fontSize: 11,
|
||||||
fontSize: 10,
|
fontWeight: "500",
|
||||||
paddingVertical: 5,
|
paddingVertical: 2,
|
||||||
paddingHorizontal: 10,
|
paddingHorizontal: 8,
|
||||||
borderRadius: 10,
|
borderRadius: 8,
|
||||||
fontStyle: "italic",
|
backgroundColor: "rgba(255,255,255,0.15)",
|
||||||
backgroundColor: "rgba(255, 255, 255, 1)",
|
|
||||||
overflow: "hidden",
|
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;
|
export default ShowCard;
|
||||||
|
|||||||
@@ -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 = {
|
type ShowInfoProps = {
|
||||||
seasons: number;
|
seasons: number;
|
||||||
@@ -17,37 +18,42 @@ const ShowInfo = ({
|
|||||||
}: ShowInfoProps) => {
|
}: ShowInfoProps) => {
|
||||||
return (
|
return (
|
||||||
<View style={styles.showMainInfoSection}>
|
<View style={styles.showMainInfoSection}>
|
||||||
<Text style={styles.showInfoText}>{seasons} Staffeln</Text>
|
<BlurView intensity={25} tint="dark" style={styles.pill}>
|
||||||
<View style={styles.dot} />
|
<Text style={styles.showInfoText}>{seasons} Staffeln</Text>
|
||||||
<Text style={styles.showInfoText}>{participants} Teilnehmer</Text>
|
</BlurView>
|
||||||
<View style={styles.dot} />
|
<BlurView intensity={25} tint="dark" style={styles.pill}>
|
||||||
<Text style={styles.showInfoText}>{streamingService}</Text>
|
<Text style={styles.showInfoText}>{participants} Teilnehmer</Text>
|
||||||
|
</BlurView>
|
||||||
|
<BlurView intensity={25} tint="dark" style={styles.pill}>
|
||||||
|
<Text style={styles.showInfoText}>{streamingService}</Text>
|
||||||
|
</BlurView>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
showMainInfoSection: {
|
showMainInfoSection: {
|
||||||
width: "auto",
|
|
||||||
height: "auto",
|
|
||||||
alignSelf: "center",
|
alignSelf: "center",
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
top: 20,
|
gap: 8,
|
||||||
marginBottom: 20,
|
marginTop: 8,
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
pill: {
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
paddingVertical: 7,
|
||||||
|
borderRadius: 20,
|
||||||
|
overflow: "hidden",
|
||||||
|
borderWidth: StyleSheet.hairlineWidth,
|
||||||
|
borderColor: "rgba(255,255,255,0.1)",
|
||||||
},
|
},
|
||||||
showInfoText: {
|
showInfoText: {
|
||||||
color: "hsl(0, 0%, 80%)",
|
color: "rgba(255,255,255,0.85)",
|
||||||
fontSize: 14,
|
fontSize: 13,
|
||||||
},
|
fontWeight: "500",
|
||||||
dot: {
|
letterSpacing: 0.2,
|
||||||
width: 4,
|
|
||||||
height: 4,
|
|
||||||
borderRadius: 3,
|
|
||||||
backgroundColor: "hsl(0, 0%, 80%)",
|
|
||||||
marginHorizontal: 7,
|
|
||||||
marginTop: 2,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Feather from "@expo/vector-icons/Feather";
|
import Feather from "@expo/vector-icons/Feather";
|
||||||
|
import { BlurView } from "expo-blur";
|
||||||
import { router, useLocalSearchParams } from "expo-router";
|
import { router, useLocalSearchParams } from "expo-router";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {
|
import {
|
||||||
@@ -15,48 +16,51 @@ export default function StackHeader() {
|
|||||||
const logoUriString = Array.isArray(logoUri) ? logoUri[0] : logoUri;
|
const logoUriString = Array.isArray(logoUri) ? logoUri[0] : logoUri;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.header}>
|
<BlurView intensity={60} tint="dark" style={styles.header}>
|
||||||
<TouchableOpacity onPress={() => router.back()}>
|
<TouchableOpacity onPress={() => router.back()}>
|
||||||
<Feather name="arrow-left" size={26} color="white" />
|
<BlurView intensity={40} tint="light" style={styles.backButton}>
|
||||||
|
<Feather
|
||||||
|
name="chevron-left"
|
||||||
|
size={22}
|
||||||
|
color="rgba(255,255,255,0.95)"
|
||||||
|
/>
|
||||||
|
</BlurView>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Image style={styles.logo} source={{ uri: logoUriString }} />
|
<Image style={styles.logo} source={{ uri: logoUriString }} />
|
||||||
{/* <TouchableOpacity>
|
<View style={{ width: 40 }} />
|
||||||
<Feather name="share" size={26} color="white" />
|
</BlurView>
|
||||||
</TouchableOpacity> */}
|
|
||||||
</View>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
header: {
|
header: {
|
||||||
height: 150,
|
height: 140,
|
||||||
backgroundColor: "hsl(221, 39%, 12%)",
|
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
borderBottomWidth: 1,
|
paddingTop: Dimensions.get("window").height * 0.06,
|
||||||
paddingTop: Dimensions.get("window").height * 0.065,
|
paddingHorizontal: 16,
|
||||||
paddingHorizontal: 20,
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
|
borderBottomColor: "rgba(255,255,255,0.08)",
|
||||||
borderBottomColor: "hsl(221, 39%, 15%)",
|
},
|
||||||
shadowColor: "#000",
|
backButton: {
|
||||||
shadowOffset: {
|
width: 40,
|
||||||
width: 0,
|
height: 40,
|
||||||
height: 3,
|
borderRadius: 20,
|
||||||
},
|
overflow: "hidden",
|
||||||
shadowOpacity: 0.25,
|
justifyContent: "center",
|
||||||
shadowRadius: 3.84,
|
alignItems: "center",
|
||||||
elevation: 5,
|
borderWidth: StyleSheet.hairlineWidth,
|
||||||
|
borderColor: "rgba(255,255,255,0.18)",
|
||||||
},
|
},
|
||||||
logo: {
|
logo: {
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 100,
|
height: 100,
|
||||||
resizeMode: "contain",
|
resizeMode: "contain",
|
||||||
marginLeft: 10,
|
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
color: "white",
|
color: "white",
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: "bold",
|
fontWeight: "600",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
|
import { discoverSearch } from "@/apis/searchApi";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { getSearchResults } from "@/apis/searchApi";
|
|
||||||
|
|
||||||
export const useSearch = (tags: string[]) => {
|
export const useSearch = (tags: string[]) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["search", tags],
|
queryKey: ["search", tags],
|
||||||
queryFn: () => getSearchResults(tags),
|
queryFn: () => discoverSearch(tags),
|
||||||
enabled: tags.length > 0,
|
enabled: tags.length > 0,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export function mapApiPersonToUI(data: any) {
|
|||||||
export function mapApiSeasonToUI(data: any) {
|
export function mapApiSeasonToUI(data: any) {
|
||||||
return {
|
return {
|
||||||
seasonId: data?.seasonId ?? data?.id,
|
seasonId: data?.seasonId ?? data?.id,
|
||||||
showId: data?.showId,
|
showId: data?.showId ?? data?.show,
|
||||||
startDate: data?.startDate ?? null,
|
startDate: data?.startDate ?? null,
|
||||||
endDate: data?.endDate ?? null,
|
endDate: data?.endDate ?? null,
|
||||||
seasonNumber: data?.seasonNumber ?? null,
|
seasonNumber: data?.seasonNumber ?? null,
|
||||||
@@ -40,12 +40,20 @@ export function mapApiSeasonToUI(data: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function mapApiShowToUI(data: any) {
|
export function mapApiShowToUI(data: any) {
|
||||||
|
const id = data?.showId ?? data?.id;
|
||||||
|
const genre = data?.genre ?? "";
|
||||||
return {
|
return {
|
||||||
showId: data?.showId ?? data?.id,
|
id,
|
||||||
title: data?.title ?? data?.name ?? `Show #${data?.showId ?? data?.id ?? "?"}`,
|
showId: id,
|
||||||
|
title: data?.title ?? data?.name ?? `Show #${id ?? "?"}`,
|
||||||
description: data?.description ?? "",
|
description: data?.description ?? "",
|
||||||
genre: data?.genre ?? "",
|
genres: genre ? genre.split(",").map((g: string) => g.trim()) : [],
|
||||||
thumbnailUrl: data?.thumbnailUrl ?? data?.imageUrl ?? "",
|
genre,
|
||||||
|
thumbnailUri: data?.thumbnailUrl ?? data?.imageUrl ?? "",
|
||||||
|
bannerUri: data?.bannerUrl ?? "",
|
||||||
|
streamingService: data?.streamingServices ?? "",
|
||||||
|
concept: data?.concept ?? "",
|
||||||
running: data?.running ?? false,
|
running: data?.running ?? false,
|
||||||
|
logoUrl: data?.logoUrl ?? "",
|
||||||
} as any;
|
} as any;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user