Compare commits
10 Commits
784eb3afe1
...
4a642c7e5d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a642c7e5d | ||
|
|
c67e60a57b | ||
|
|
44e3558681 | ||
|
|
a076e856ad | ||
|
|
9516642beb | ||
|
|
b287f19686 | ||
|
|
37aa3008c6 | ||
|
|
09a58dd656 | ||
|
|
63995d2be0 | ||
|
|
9b1ded46f8 |
@@ -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 [];
|
||||||
|
|||||||
12
app.json
12
app.json
@@ -2,15 +2,21 @@
|
|||||||
"expo": {
|
"expo": {
|
||||||
"name": "FLTR",
|
"name": "FLTR",
|
||||||
"slug": "fltr-app",
|
"slug": "fltr-app",
|
||||||
"version": "1.0.1",
|
"version": "1.0.4",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/images/icon.png",
|
"icon": "./assets/images/icon.png",
|
||||||
"scheme": "fltrapp",
|
"scheme": "fltrapp",
|
||||||
"userInterfaceStyle": "automatic",
|
"userInterfaceStyle": "automatic",
|
||||||
"newArchEnabled": true,
|
"newArchEnabled": true,
|
||||||
"ios": {
|
"ios": {
|
||||||
"supportsTablet": true,
|
"supportsTablet": false,
|
||||||
"bundleIdentifier": "de.berg-autosoft.fltr"
|
"bundleIdentifier": "de.berg-autosoft.fltr",
|
||||||
|
"infoPlist": {
|
||||||
|
"ITSAppUsesNonExemptEncryption": false,
|
||||||
|
"NSAppTransportSecurity": {
|
||||||
|
"NSAllowsArbitraryLoads": true
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
|
|||||||
@@ -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,
|
|
||||||
paddingTop: 10,
|
|
||||||
},
|
|
||||||
tabBarInactiveTintColor: "hsl(0, 0%, 100%)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tabs.Screen
|
|
||||||
name="index"
|
|
||||||
options={{
|
|
||||||
title: "Home",
|
|
||||||
tabBarIcon: ({ color, size }) => (
|
|
||||||
<Feather name="home" size={size} color={color} />
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Tabs.Screen
|
</NativeTabs.Trigger>
|
||||||
name="explore"
|
<NativeTabs.Trigger name="explore">
|
||||||
options={{
|
<Label>Durchsuchen</Label>
|
||||||
title: "Durchsuchen",
|
<Icon
|
||||||
tabBarIcon: ({ color, size }) => (
|
sf="magnifyingglass"
|
||||||
<Feather name="search" size={size} color={color} />
|
androidSrc={<VectorIcon family={Feather} name="search" />}
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Tabs>
|
</NativeTabs.Trigger>
|
||||||
|
</NativeTabs>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,307 +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" },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text>Error: {error?.message || String(error)}</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<GestureHandlerRootView>
|
|
||||||
<View style={styles.mainContainer}>
|
|
||||||
<View style={styles.header}>
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => {
|
|
||||||
haptikFeedback();
|
|
||||||
router.push("/legal");
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
left: 16,
|
|
||||||
top: "50%",
|
|
||||||
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="menu" 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>
|
</View>
|
||||||
|
|
||||||
{/* Support Card */}
|
<View style={styles.divider} />
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
backgroundColor: "rgba(255,255,255,0.05)",
|
|
||||||
borderRadius: 14,
|
|
||||||
padding: 16,
|
|
||||||
gap: 10,
|
|
||||||
marginTop: 12,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text style={{ color: "white", fontSize: 18, fontWeight: "700" }}>
|
|
||||||
Support
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text style={styles.body}>
|
<View style={styles.cardSection}>
|
||||||
|
<Text style={styles.textDim}>Steuernummer: 25/103/17193</Text>
|
||||||
|
<Text style={styles.textDim}>USt-ID: DE361689728</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Support */}
|
||||||
|
<View style={styles.card}>
|
||||||
|
<View style={styles.cardHeader}>
|
||||||
|
<View style={styles.cardIconCircle}>
|
||||||
|
<Feather name="headphones" size={16} color="#199edb" />
|
||||||
|
</View>
|
||||||
|
<Text style={styles.cardTitle}>Support</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<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,45 +54,79 @@ 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 }}
|
||||||
|
|
||||||
>
|
>
|
||||||
|
{/* Profile Hero */}
|
||||||
|
<View style={styles.profileHero}>
|
||||||
|
<View style={styles.profileImageContainer}>
|
||||||
|
<Image
|
||||||
|
source={{ uri: imageUriString || undefined }}
|
||||||
|
style={styles.profileImage}
|
||||||
|
resizeMode="cover"
|
||||||
|
blurRadius={isPravatar ? 16 : 0}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
<Text style={styles.participantName}>{name}</Text>
|
<Text style={styles.participantName}>{name}</Text>
|
||||||
<TouchableOpacity
|
<Text style={styles.participantSubtitle}>
|
||||||
style={styles.closeIcon}
|
{appearances.length}{" "}
|
||||||
onPress={() => router.back()}
|
{appearances.length === 1 ? "Auftritt" : "Auftritte"}
|
||||||
>
|
</Text>
|
||||||
<Ionicons name="close-circle-outline" size={38} color="white" />
|
<View style={styles.heroButtons}>
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<View style={styles.performedShowsSection}>
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.searchButton}
|
style={styles.searchButton}
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
WebBrowser.openBrowserAsync(
|
WebBrowser.openBrowserAsync(
|
||||||
"https://www.google.com/search?udm=2&q=" +
|
"https://www.google.com/search?udm=2&q=" +
|
||||||
encodeURIComponent(String(name))
|
encodeURIComponent(String(name)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Ionicons name="images-outline" size={24} color="white" />
|
<Ionicons name="images-outline" size={18} color="#199edb" />
|
||||||
|
<Text style={styles.searchButtonText}>Bilder</Text>
|
||||||
</TouchableOpacity>
|
</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>
|
||||||
|
|
||||||
<Text style={styles.performedShowsTitle}>Auftritte:</Text>
|
{/* Loading */}
|
||||||
|
{isLoading && (
|
||||||
|
<View style={styles.loadingContainer}>
|
||||||
|
<ActivityIndicator size="large" color="#199edb" />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Appearances */}
|
||||||
|
{!isLoading && appearances.length > 0 && (
|
||||||
|
<View style={styles.performedShowsSection}>
|
||||||
|
<Text style={styles.performedShowsTitle}>Auftritte</Text>
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
horizontal
|
horizontal
|
||||||
@@ -91,8 +134,8 @@ export default function ParticipantScreen() {
|
|||||||
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 }) => {
|
||||||
@@ -101,8 +144,8 @@ export default function ParticipantScreen() {
|
|||||||
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(
|
||||||
@@ -110,8 +153,8 @@ export default function ParticipantScreen() {
|
|||||||
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);
|
||||||
@@ -120,7 +163,7 @@ export default function ParticipantScreen() {
|
|||||||
: 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 (
|
||||||
@@ -135,23 +178,71 @@ export default function ParticipantScreen() {
|
|||||||
resizeMode="cover"
|
resizeMode="cover"
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<View style={styles.cardInfo}>
|
||||||
|
<View style={styles.cardTitleRow}>
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
<Text style={styles.showTitle} numberOfLines={1}>
|
<Text style={styles.showTitle} numberOfLines={1}>
|
||||||
{show.title}
|
{show.title}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.showSeason}>
|
<Text style={styles.showSeason}>
|
||||||
({formatYear(seasons[0]?.startDate)})
|
Staffel{" "}
|
||||||
|
{seasons.map((s) => s.seasonNumber).join(" & ")}
|
||||||
|
{" · "}
|
||||||
|
{formatYear(seasons[0]?.startDate)}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.showSeason}>
|
</View>
|
||||||
Staffel {seasons.map((s) => s.seasonNumber).join(" und ")}
|
<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>
|
||||||
|
</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>
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
<View style={styles.horizontalLine} />
|
{allParticipants.length > 0 && (
|
||||||
|
<View style={styles.participantsSection}>
|
||||||
<Text style={[styles.participantLabel, { marginTop: 10 }]}>
|
<Text style={styles.sectionLabel}>
|
||||||
Weitere Teilnehmer
|
Weitere Teilnehmer
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<View style={styles.participantContainer}>
|
|
||||||
<View style={styles.participantRow}>
|
<View style={styles.participantRow}>
|
||||||
{visible.map((p) => (
|
{visible.map((p) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@@ -189,49 +280,14 @@ export default function ParticipantScreen() {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{partners.length > 0 && (
|
|
||||||
<>
|
|
||||||
<View style={styles.horizontalLine} />
|
|
||||||
<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>
|
</View>
|
||||||
|
</View>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</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,29 +65,45 @@ 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 ? (
|
||||||
|
<View style={styles.loadingContainer}>
|
||||||
|
<ActivityIndicator size="large" color="#199edb" />
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
<ScrollView
|
<ScrollView
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
contentContainerStyle={{
|
contentContainerStyle={{
|
||||||
paddingBottom: Dimensions.get("window").height * 0.1,
|
paddingBottom: Dimensions.get("window").height * 0.1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{logoUriString ? (
|
||||||
|
<View style={styles.logoContainer}>
|
||||||
|
<Image
|
||||||
|
source={{ uri: logoUriString }}
|
||||||
|
style={styles.showLogo}
|
||||||
|
resizeMode="contain"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
{formattedStartDate ? (
|
{formattedStartDate ? (
|
||||||
<Text style={styles.startDate}>{formattedStartDate}</Text>
|
<Text style={styles.startDate}>{formattedStartDate}</Text>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -106,26 +125,54 @@ export default function ShowDetails() {
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.infoContainner}>
|
<View style={styles.infoContainner}>
|
||||||
<TouchableOpacity onPress={() => setSelectedParticipants(true)}>
|
<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
|
<Text
|
||||||
style={[
|
style={[
|
||||||
styles.infoLabel,
|
styles.infoLabel,
|
||||||
{
|
{
|
||||||
fontWeight: selectedParticipants ? "bold" : "normal",
|
fontWeight: selectedParticipants ? "700" : "500",
|
||||||
color: selectedParticipants ? "#199edb" : "hsl(0, 0%, 65%)",
|
color: selectedParticipants
|
||||||
|
? "#199edb"
|
||||||
|
: "rgba(255,255,255,0.45)",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
Teilnehmer
|
Teilnehmer
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity onPress={() => setSelectedParticipants(false)}>
|
<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
|
<Text
|
||||||
style={[
|
style={[
|
||||||
styles.infoLabel,
|
styles.infoLabel,
|
||||||
{
|
{
|
||||||
fontWeight: !selectedParticipants ? "bold" : "normal",
|
fontWeight: !selectedParticipants ? "700" : "500",
|
||||||
color: !selectedParticipants ? "#199edb" : "hsl(0, 0%, 65%)",
|
color: !selectedParticipants
|
||||||
|
? "#199edb"
|
||||||
|
: "rgba(255,255,255,0.45)",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -152,17 +199,23 @@ export default function ShowDetails() {
|
|||||||
backgroundColor:
|
backgroundColor:
|
||||||
selectedSeason === season
|
selectedSeason === season
|
||||||
? "#199edb"
|
? "#199edb"
|
||||||
: "hsl(0, 0%, 20%)",
|
: "rgba(255,255,255,0.08)",
|
||||||
|
borderColor:
|
||||||
|
selectedSeason === season
|
||||||
|
? "rgba(25,158,219,0.3)"
|
||||||
|
: "rgba(255,255,255,0.06)",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setSelectedSeason(season);
|
setSelectedSeason(season);
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
Haptics.impactAsync(
|
||||||
|
Haptics.ImpactFeedbackStyle.Light,
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={styles.seasonLabel}>{season}</Text>
|
<Text style={styles.seasonLabel}>{season}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)
|
),
|
||||||
)}
|
)}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
@@ -174,28 +227,48 @@ export default function ShowDetails() {
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{pError && (
|
{pError && (
|
||||||
<Text style={{ color: "tomato", marginBottom: 8 }}>
|
<Text
|
||||||
|
style={{ color: "tomato", marginBottom: 8, fontSize: 13 }}
|
||||||
|
>
|
||||||
{pError}
|
{pError}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
{!pLoading && !pError && participants.length === 0 && (
|
{pLoading && (
|
||||||
<Text style={{ color: "gray" }}>Keine Teilnehmer.</Text>
|
<View style={styles.sectionLoading}>
|
||||||
|
<ActivityIndicator size="small" color="#199edb" />
|
||||||
|
</View>
|
||||||
)}
|
)}
|
||||||
{sortedParticipants.map((p) => (
|
{!pLoading && !pError && participants.length === 0 && (
|
||||||
|
<Text
|
||||||
|
style={{ color: "rgba(255,255,255,0.4)", fontSize: 14 }}
|
||||||
|
>
|
||||||
|
Keine Teilnehmer.
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{!pLoading &&
|
||||||
|
sortedParticipants.map((p) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={p.id}
|
key={p.id}
|
||||||
|
style={styles.participantWrapper}
|
||||||
|
onPress={() => handleOpenParticipant(p)}
|
||||||
|
>
|
||||||
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.participantContainer,
|
styles.participantContainer,
|
||||||
{ backgroundColor: "hsl(336, 79%, 63%)" },
|
{ backgroundColor: "hsl(336, 79%, 63%)" },
|
||||||
]}
|
]}
|
||||||
onPress={() => handleOpenParticipant(p)}
|
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: p.imageUri }}
|
source={{ uri: p.imageUri }}
|
||||||
style={{ width: "100%", height: "100%", borderRadius: 10 }}
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
borderRadius: 16,
|
||||||
|
}}
|
||||||
resizeMode="cover"
|
resizeMode="cover"
|
||||||
blurRadius={p.imageUri.includes("pravatar") ? 16 : 0}
|
blurRadius={p.imageUri.includes("pravatar") ? 16 : 0}
|
||||||
/>
|
/>
|
||||||
|
</View>
|
||||||
<Text style={styles.participantLabel} numberOfLines={2}>
|
<Text style={styles.participantLabel} numberOfLines={2}>
|
||||||
{p.name}
|
{p.name}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -212,6 +285,7 @@ export default function ShowDetails() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</ScrollView>
|
</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,
|
||||||
|
|||||||
514
build/config.gypi
Normal file
514
build/config.gypi
Normal file
@@ -0,0 +1,514 @@
|
|||||||
|
# Do not edit. File was generated by node-gyp's "configure" step
|
||||||
|
{
|
||||||
|
"target_defaults": {
|
||||||
|
"cflags": [],
|
||||||
|
"configurations": {
|
||||||
|
"Debug": {
|
||||||
|
"v8_enable_v8_checks": 0,
|
||||||
|
"variables": {}
|
||||||
|
},
|
||||||
|
"Release": {
|
||||||
|
"v8_enable_v8_checks": 1,
|
||||||
|
"variables": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default_configuration": "Release",
|
||||||
|
"defines": [],
|
||||||
|
"include_dirs": [],
|
||||||
|
"libraries": [],
|
||||||
|
"msvs_configuration_platform": "ARM64",
|
||||||
|
"xcode_configuration_platform": "arm64"
|
||||||
|
},
|
||||||
|
"variables": {
|
||||||
|
"arm_fpu": "neon",
|
||||||
|
"asan": 0,
|
||||||
|
"clang": 1,
|
||||||
|
"control_flow_guard": "false",
|
||||||
|
"coverage": "false",
|
||||||
|
"dcheck_always_on": 0,
|
||||||
|
"debug_nghttp2": "false",
|
||||||
|
"debug_node": "false",
|
||||||
|
"enable_lto": "false",
|
||||||
|
"enable_pgo_generate": "false",
|
||||||
|
"enable_pgo_use": "false",
|
||||||
|
"error_on_warn": "false",
|
||||||
|
"force_dynamic_crt": 0,
|
||||||
|
"host_arch": "arm64",
|
||||||
|
"icu_data_in": "../../deps/icu-tmp/icudt77l.dat",
|
||||||
|
"icu_endianness": "l",
|
||||||
|
"icu_gyp_path": "tools/icu/icu-generic.gyp",
|
||||||
|
"icu_path": "deps/icu-small",
|
||||||
|
"icu_small": "false",
|
||||||
|
"icu_ver_major": "77",
|
||||||
|
"libdir": "lib",
|
||||||
|
"llvm_version": "16.0",
|
||||||
|
"napi_build_version": "10",
|
||||||
|
"node_builtin_shareable_builtins": [
|
||||||
|
"deps/cjs-module-lexer/lexer.js",
|
||||||
|
"deps/cjs-module-lexer/dist/lexer.js",
|
||||||
|
"deps/undici/undici.js",
|
||||||
|
"deps/amaro/dist/index.js"
|
||||||
|
],
|
||||||
|
"node_byteorder": "little",
|
||||||
|
"node_cctest_sources": [
|
||||||
|
"src/node_snapshot_stub.cc",
|
||||||
|
"test/cctest/inspector/test_node_protocol.cc",
|
||||||
|
"test/cctest/node_test_fixture.cc",
|
||||||
|
"test/cctest/test_aliased_buffer.cc",
|
||||||
|
"test/cctest/test_base64.cc",
|
||||||
|
"test/cctest/test_base_object_ptr.cc",
|
||||||
|
"test/cctest/test_cppgc.cc",
|
||||||
|
"test/cctest/test_crypto_clienthello.cc",
|
||||||
|
"test/cctest/test_dataqueue.cc",
|
||||||
|
"test/cctest/test_environment.cc",
|
||||||
|
"test/cctest/test_inspector_socket.cc",
|
||||||
|
"test/cctest/test_inspector_socket_server.cc",
|
||||||
|
"test/cctest/test_json_utils.cc",
|
||||||
|
"test/cctest/test_linked_binding.cc",
|
||||||
|
"test/cctest/test_node_api.cc",
|
||||||
|
"test/cctest/test_node_crypto.cc",
|
||||||
|
"test/cctest/test_node_crypto_env.cc",
|
||||||
|
"test/cctest/test_node_postmortem_metadata.cc",
|
||||||
|
"test/cctest/test_node_task_runner.cc",
|
||||||
|
"test/cctest/test_path.cc",
|
||||||
|
"test/cctest/test_per_process.cc",
|
||||||
|
"test/cctest/test_platform.cc",
|
||||||
|
"test/cctest/test_quic_cid.cc",
|
||||||
|
"test/cctest/test_quic_error.cc",
|
||||||
|
"test/cctest/test_quic_tokens.cc",
|
||||||
|
"test/cctest/test_report.cc",
|
||||||
|
"test/cctest/test_sockaddr.cc",
|
||||||
|
"test/cctest/test_traced_value.cc",
|
||||||
|
"test/cctest/test_util.cc",
|
||||||
|
"test/cctest/node_test_fixture.h"
|
||||||
|
],
|
||||||
|
"node_debug_lib": "false",
|
||||||
|
"node_enable_d8": "false",
|
||||||
|
"node_enable_v8_vtunejit": "false",
|
||||||
|
"node_enable_v8windbg": "false",
|
||||||
|
"node_fipsinstall": "false",
|
||||||
|
"node_install_corepack": "true",
|
||||||
|
"node_install_npm": "true",
|
||||||
|
"node_library_files": [
|
||||||
|
"lib/_http_agent.js",
|
||||||
|
"lib/_http_client.js",
|
||||||
|
"lib/_http_common.js",
|
||||||
|
"lib/_http_incoming.js",
|
||||||
|
"lib/_http_outgoing.js",
|
||||||
|
"lib/_http_server.js",
|
||||||
|
"lib/_stream_duplex.js",
|
||||||
|
"lib/_stream_passthrough.js",
|
||||||
|
"lib/_stream_readable.js",
|
||||||
|
"lib/_stream_transform.js",
|
||||||
|
"lib/_stream_wrap.js",
|
||||||
|
"lib/_stream_writable.js",
|
||||||
|
"lib/_tls_common.js",
|
||||||
|
"lib/_tls_wrap.js",
|
||||||
|
"lib/assert.js",
|
||||||
|
"lib/assert/strict.js",
|
||||||
|
"lib/async_hooks.js",
|
||||||
|
"lib/buffer.js",
|
||||||
|
"lib/child_process.js",
|
||||||
|
"lib/cluster.js",
|
||||||
|
"lib/console.js",
|
||||||
|
"lib/constants.js",
|
||||||
|
"lib/crypto.js",
|
||||||
|
"lib/dgram.js",
|
||||||
|
"lib/diagnostics_channel.js",
|
||||||
|
"lib/dns.js",
|
||||||
|
"lib/dns/promises.js",
|
||||||
|
"lib/domain.js",
|
||||||
|
"lib/events.js",
|
||||||
|
"lib/fs.js",
|
||||||
|
"lib/fs/promises.js",
|
||||||
|
"lib/http.js",
|
||||||
|
"lib/http2.js",
|
||||||
|
"lib/https.js",
|
||||||
|
"lib/inspector.js",
|
||||||
|
"lib/inspector/promises.js",
|
||||||
|
"lib/internal/abort_controller.js",
|
||||||
|
"lib/internal/assert.js",
|
||||||
|
"lib/internal/assert/assertion_error.js",
|
||||||
|
"lib/internal/assert/calltracker.js",
|
||||||
|
"lib/internal/assert/myers_diff.js",
|
||||||
|
"lib/internal/assert/utils.js",
|
||||||
|
"lib/internal/async_context_frame.js",
|
||||||
|
"lib/internal/async_hooks.js",
|
||||||
|
"lib/internal/async_local_storage/async_context_frame.js",
|
||||||
|
"lib/internal/async_local_storage/async_hooks.js",
|
||||||
|
"lib/internal/blob.js",
|
||||||
|
"lib/internal/blocklist.js",
|
||||||
|
"lib/internal/bootstrap/node.js",
|
||||||
|
"lib/internal/bootstrap/realm.js",
|
||||||
|
"lib/internal/bootstrap/shadow_realm.js",
|
||||||
|
"lib/internal/bootstrap/switches/does_not_own_process_state.js",
|
||||||
|
"lib/internal/bootstrap/switches/does_own_process_state.js",
|
||||||
|
"lib/internal/bootstrap/switches/is_main_thread.js",
|
||||||
|
"lib/internal/bootstrap/switches/is_not_main_thread.js",
|
||||||
|
"lib/internal/bootstrap/web/exposed-wildcard.js",
|
||||||
|
"lib/internal/bootstrap/web/exposed-window-or-worker.js",
|
||||||
|
"lib/internal/buffer.js",
|
||||||
|
"lib/internal/child_process.js",
|
||||||
|
"lib/internal/child_process/serialization.js",
|
||||||
|
"lib/internal/cli_table.js",
|
||||||
|
"lib/internal/cluster/child.js",
|
||||||
|
"lib/internal/cluster/primary.js",
|
||||||
|
"lib/internal/cluster/round_robin_handle.js",
|
||||||
|
"lib/internal/cluster/shared_handle.js",
|
||||||
|
"lib/internal/cluster/utils.js",
|
||||||
|
"lib/internal/cluster/worker.js",
|
||||||
|
"lib/internal/console/constructor.js",
|
||||||
|
"lib/internal/console/global.js",
|
||||||
|
"lib/internal/constants.js",
|
||||||
|
"lib/internal/crypto/aes.js",
|
||||||
|
"lib/internal/crypto/certificate.js",
|
||||||
|
"lib/internal/crypto/cfrg.js",
|
||||||
|
"lib/internal/crypto/cipher.js",
|
||||||
|
"lib/internal/crypto/diffiehellman.js",
|
||||||
|
"lib/internal/crypto/ec.js",
|
||||||
|
"lib/internal/crypto/hash.js",
|
||||||
|
"lib/internal/crypto/hashnames.js",
|
||||||
|
"lib/internal/crypto/hkdf.js",
|
||||||
|
"lib/internal/crypto/keygen.js",
|
||||||
|
"lib/internal/crypto/keys.js",
|
||||||
|
"lib/internal/crypto/mac.js",
|
||||||
|
"lib/internal/crypto/pbkdf2.js",
|
||||||
|
"lib/internal/crypto/random.js",
|
||||||
|
"lib/internal/crypto/rsa.js",
|
||||||
|
"lib/internal/crypto/scrypt.js",
|
||||||
|
"lib/internal/crypto/sig.js",
|
||||||
|
"lib/internal/crypto/util.js",
|
||||||
|
"lib/internal/crypto/webcrypto.js",
|
||||||
|
"lib/internal/crypto/webidl.js",
|
||||||
|
"lib/internal/crypto/x509.js",
|
||||||
|
"lib/internal/data_url.js",
|
||||||
|
"lib/internal/debugger/inspect.js",
|
||||||
|
"lib/internal/debugger/inspect_client.js",
|
||||||
|
"lib/internal/debugger/inspect_repl.js",
|
||||||
|
"lib/internal/dgram.js",
|
||||||
|
"lib/internal/dns/callback_resolver.js",
|
||||||
|
"lib/internal/dns/promises.js",
|
||||||
|
"lib/internal/dns/utils.js",
|
||||||
|
"lib/internal/encoding.js",
|
||||||
|
"lib/internal/error_serdes.js",
|
||||||
|
"lib/internal/errors.js",
|
||||||
|
"lib/internal/errors/error_source.js",
|
||||||
|
"lib/internal/event_target.js",
|
||||||
|
"lib/internal/events/abort_listener.js",
|
||||||
|
"lib/internal/events/symbols.js",
|
||||||
|
"lib/internal/file.js",
|
||||||
|
"lib/internal/fixed_queue.js",
|
||||||
|
"lib/internal/freelist.js",
|
||||||
|
"lib/internal/freeze_intrinsics.js",
|
||||||
|
"lib/internal/fs/cp/cp-sync.js",
|
||||||
|
"lib/internal/fs/cp/cp.js",
|
||||||
|
"lib/internal/fs/dir.js",
|
||||||
|
"lib/internal/fs/glob.js",
|
||||||
|
"lib/internal/fs/promises.js",
|
||||||
|
"lib/internal/fs/read/context.js",
|
||||||
|
"lib/internal/fs/recursive_watch.js",
|
||||||
|
"lib/internal/fs/rimraf.js",
|
||||||
|
"lib/internal/fs/streams.js",
|
||||||
|
"lib/internal/fs/sync_write_stream.js",
|
||||||
|
"lib/internal/fs/utils.js",
|
||||||
|
"lib/internal/fs/watchers.js",
|
||||||
|
"lib/internal/heap_utils.js",
|
||||||
|
"lib/internal/histogram.js",
|
||||||
|
"lib/internal/http.js",
|
||||||
|
"lib/internal/http2/compat.js",
|
||||||
|
"lib/internal/http2/core.js",
|
||||||
|
"lib/internal/http2/util.js",
|
||||||
|
"lib/internal/inspector/network.js",
|
||||||
|
"lib/internal/inspector/network_http.js",
|
||||||
|
"lib/internal/inspector/network_http2.js",
|
||||||
|
"lib/internal/inspector/network_resources.js",
|
||||||
|
"lib/internal/inspector/network_undici.js",
|
||||||
|
"lib/internal/inspector_async_hook.js",
|
||||||
|
"lib/internal/inspector_network_tracking.js",
|
||||||
|
"lib/internal/js_stream_socket.js",
|
||||||
|
"lib/internal/legacy/processbinding.js",
|
||||||
|
"lib/internal/linkedlist.js",
|
||||||
|
"lib/internal/main/check_syntax.js",
|
||||||
|
"lib/internal/main/embedding.js",
|
||||||
|
"lib/internal/main/eval_stdin.js",
|
||||||
|
"lib/internal/main/eval_string.js",
|
||||||
|
"lib/internal/main/inspect.js",
|
||||||
|
"lib/internal/main/mksnapshot.js",
|
||||||
|
"lib/internal/main/print_help.js",
|
||||||
|
"lib/internal/main/prof_process.js",
|
||||||
|
"lib/internal/main/repl.js",
|
||||||
|
"lib/internal/main/run_main_module.js",
|
||||||
|
"lib/internal/main/test_runner.js",
|
||||||
|
"lib/internal/main/watch_mode.js",
|
||||||
|
"lib/internal/main/worker_thread.js",
|
||||||
|
"lib/internal/mime.js",
|
||||||
|
"lib/internal/modules/cjs/loader.js",
|
||||||
|
"lib/internal/modules/customization_hooks.js",
|
||||||
|
"lib/internal/modules/esm/assert.js",
|
||||||
|
"lib/internal/modules/esm/create_dynamic_module.js",
|
||||||
|
"lib/internal/modules/esm/formats.js",
|
||||||
|
"lib/internal/modules/esm/get_format.js",
|
||||||
|
"lib/internal/modules/esm/hooks.js",
|
||||||
|
"lib/internal/modules/esm/initialize_import_meta.js",
|
||||||
|
"lib/internal/modules/esm/load.js",
|
||||||
|
"lib/internal/modules/esm/loader.js",
|
||||||
|
"lib/internal/modules/esm/module_job.js",
|
||||||
|
"lib/internal/modules/esm/module_map.js",
|
||||||
|
"lib/internal/modules/esm/resolve.js",
|
||||||
|
"lib/internal/modules/esm/shared_constants.js",
|
||||||
|
"lib/internal/modules/esm/translators.js",
|
||||||
|
"lib/internal/modules/esm/utils.js",
|
||||||
|
"lib/internal/modules/esm/worker.js",
|
||||||
|
"lib/internal/modules/helpers.js",
|
||||||
|
"lib/internal/modules/package_json_reader.js",
|
||||||
|
"lib/internal/modules/run_main.js",
|
||||||
|
"lib/internal/modules/typescript.js",
|
||||||
|
"lib/internal/navigator.js",
|
||||||
|
"lib/internal/net.js",
|
||||||
|
"lib/internal/options.js",
|
||||||
|
"lib/internal/per_context/domexception.js",
|
||||||
|
"lib/internal/per_context/messageport.js",
|
||||||
|
"lib/internal/per_context/primordials.js",
|
||||||
|
"lib/internal/perf/event_loop_delay.js",
|
||||||
|
"lib/internal/perf/event_loop_utilization.js",
|
||||||
|
"lib/internal/perf/nodetiming.js",
|
||||||
|
"lib/internal/perf/observe.js",
|
||||||
|
"lib/internal/perf/performance.js",
|
||||||
|
"lib/internal/perf/performance_entry.js",
|
||||||
|
"lib/internal/perf/resource_timing.js",
|
||||||
|
"lib/internal/perf/timerify.js",
|
||||||
|
"lib/internal/perf/usertiming.js",
|
||||||
|
"lib/internal/perf/utils.js",
|
||||||
|
"lib/internal/priority_queue.js",
|
||||||
|
"lib/internal/process/execution.js",
|
||||||
|
"lib/internal/process/finalization.js",
|
||||||
|
"lib/internal/process/per_thread.js",
|
||||||
|
"lib/internal/process/permission.js",
|
||||||
|
"lib/internal/process/pre_execution.js",
|
||||||
|
"lib/internal/process/promises.js",
|
||||||
|
"lib/internal/process/report.js",
|
||||||
|
"lib/internal/process/signal.js",
|
||||||
|
"lib/internal/process/task_queues.js",
|
||||||
|
"lib/internal/process/warning.js",
|
||||||
|
"lib/internal/process/worker_thread_only.js",
|
||||||
|
"lib/internal/promise_hooks.js",
|
||||||
|
"lib/internal/querystring.js",
|
||||||
|
"lib/internal/quic/quic.js",
|
||||||
|
"lib/internal/quic/state.js",
|
||||||
|
"lib/internal/quic/stats.js",
|
||||||
|
"lib/internal/quic/symbols.js",
|
||||||
|
"lib/internal/readline/callbacks.js",
|
||||||
|
"lib/internal/readline/emitKeypressEvents.js",
|
||||||
|
"lib/internal/readline/interface.js",
|
||||||
|
"lib/internal/readline/promises.js",
|
||||||
|
"lib/internal/readline/utils.js",
|
||||||
|
"lib/internal/repl.js",
|
||||||
|
"lib/internal/repl/await.js",
|
||||||
|
"lib/internal/repl/history.js",
|
||||||
|
"lib/internal/repl/utils.js",
|
||||||
|
"lib/internal/socket_list.js",
|
||||||
|
"lib/internal/socketaddress.js",
|
||||||
|
"lib/internal/source_map/prepare_stack_trace.js",
|
||||||
|
"lib/internal/source_map/source_map.js",
|
||||||
|
"lib/internal/source_map/source_map_cache.js",
|
||||||
|
"lib/internal/source_map/source_map_cache_map.js",
|
||||||
|
"lib/internal/stream_base_commons.js",
|
||||||
|
"lib/internal/streams/add-abort-signal.js",
|
||||||
|
"lib/internal/streams/compose.js",
|
||||||
|
"lib/internal/streams/destroy.js",
|
||||||
|
"lib/internal/streams/duplex.js",
|
||||||
|
"lib/internal/streams/duplexify.js",
|
||||||
|
"lib/internal/streams/duplexpair.js",
|
||||||
|
"lib/internal/streams/end-of-stream.js",
|
||||||
|
"lib/internal/streams/from.js",
|
||||||
|
"lib/internal/streams/lazy_transform.js",
|
||||||
|
"lib/internal/streams/legacy.js",
|
||||||
|
"lib/internal/streams/operators.js",
|
||||||
|
"lib/internal/streams/passthrough.js",
|
||||||
|
"lib/internal/streams/pipeline.js",
|
||||||
|
"lib/internal/streams/readable.js",
|
||||||
|
"lib/internal/streams/state.js",
|
||||||
|
"lib/internal/streams/transform.js",
|
||||||
|
"lib/internal/streams/utils.js",
|
||||||
|
"lib/internal/streams/writable.js",
|
||||||
|
"lib/internal/test/binding.js",
|
||||||
|
"lib/internal/test/transfer.js",
|
||||||
|
"lib/internal/test_runner/assert.js",
|
||||||
|
"lib/internal/test_runner/coverage.js",
|
||||||
|
"lib/internal/test_runner/harness.js",
|
||||||
|
"lib/internal/test_runner/mock/loader.js",
|
||||||
|
"lib/internal/test_runner/mock/mock.js",
|
||||||
|
"lib/internal/test_runner/mock/mock_timers.js",
|
||||||
|
"lib/internal/test_runner/reporter/dot.js",
|
||||||
|
"lib/internal/test_runner/reporter/junit.js",
|
||||||
|
"lib/internal/test_runner/reporter/lcov.js",
|
||||||
|
"lib/internal/test_runner/reporter/spec.js",
|
||||||
|
"lib/internal/test_runner/reporter/tap.js",
|
||||||
|
"lib/internal/test_runner/reporter/utils.js",
|
||||||
|
"lib/internal/test_runner/reporter/v8-serializer.js",
|
||||||
|
"lib/internal/test_runner/runner.js",
|
||||||
|
"lib/internal/test_runner/snapshot.js",
|
||||||
|
"lib/internal/test_runner/test.js",
|
||||||
|
"lib/internal/test_runner/tests_stream.js",
|
||||||
|
"lib/internal/test_runner/utils.js",
|
||||||
|
"lib/internal/timers.js",
|
||||||
|
"lib/internal/tls/secure-context.js",
|
||||||
|
"lib/internal/tls/secure-pair.js",
|
||||||
|
"lib/internal/trace_events_async_hooks.js",
|
||||||
|
"lib/internal/tty.js",
|
||||||
|
"lib/internal/url.js",
|
||||||
|
"lib/internal/util.js",
|
||||||
|
"lib/internal/util/colors.js",
|
||||||
|
"lib/internal/util/comparisons.js",
|
||||||
|
"lib/internal/util/debuglog.js",
|
||||||
|
"lib/internal/util/diff.js",
|
||||||
|
"lib/internal/util/inspect.js",
|
||||||
|
"lib/internal/util/inspector.js",
|
||||||
|
"lib/internal/util/parse_args/parse_args.js",
|
||||||
|
"lib/internal/util/parse_args/utils.js",
|
||||||
|
"lib/internal/util/trace_sigint.js",
|
||||||
|
"lib/internal/util/types.js",
|
||||||
|
"lib/internal/v8/startup_snapshot.js",
|
||||||
|
"lib/internal/v8_prof_polyfill.js",
|
||||||
|
"lib/internal/v8_prof_processor.js",
|
||||||
|
"lib/internal/validators.js",
|
||||||
|
"lib/internal/vm.js",
|
||||||
|
"lib/internal/vm/module.js",
|
||||||
|
"lib/internal/wasm_web_api.js",
|
||||||
|
"lib/internal/watch_mode/files_watcher.js",
|
||||||
|
"lib/internal/watchdog.js",
|
||||||
|
"lib/internal/webidl.js",
|
||||||
|
"lib/internal/webstorage.js",
|
||||||
|
"lib/internal/webstreams/adapters.js",
|
||||||
|
"lib/internal/webstreams/compression.js",
|
||||||
|
"lib/internal/webstreams/encoding.js",
|
||||||
|
"lib/internal/webstreams/queuingstrategies.js",
|
||||||
|
"lib/internal/webstreams/readablestream.js",
|
||||||
|
"lib/internal/webstreams/transfer.js",
|
||||||
|
"lib/internal/webstreams/transformstream.js",
|
||||||
|
"lib/internal/webstreams/util.js",
|
||||||
|
"lib/internal/webstreams/writablestream.js",
|
||||||
|
"lib/internal/worker.js",
|
||||||
|
"lib/internal/worker/clone_dom_exception.js",
|
||||||
|
"lib/internal/worker/io.js",
|
||||||
|
"lib/internal/worker/js_transferable.js",
|
||||||
|
"lib/internal/worker/messaging.js",
|
||||||
|
"lib/module.js",
|
||||||
|
"lib/net.js",
|
||||||
|
"lib/os.js",
|
||||||
|
"lib/path.js",
|
||||||
|
"lib/path/posix.js",
|
||||||
|
"lib/path/win32.js",
|
||||||
|
"lib/perf_hooks.js",
|
||||||
|
"lib/process.js",
|
||||||
|
"lib/punycode.js",
|
||||||
|
"lib/querystring.js",
|
||||||
|
"lib/readline.js",
|
||||||
|
"lib/readline/promises.js",
|
||||||
|
"lib/repl.js",
|
||||||
|
"lib/sea.js",
|
||||||
|
"lib/sqlite.js",
|
||||||
|
"lib/stream.js",
|
||||||
|
"lib/stream/consumers.js",
|
||||||
|
"lib/stream/promises.js",
|
||||||
|
"lib/stream/web.js",
|
||||||
|
"lib/string_decoder.js",
|
||||||
|
"lib/sys.js",
|
||||||
|
"lib/test.js",
|
||||||
|
"lib/test/reporters.js",
|
||||||
|
"lib/timers.js",
|
||||||
|
"lib/timers/promises.js",
|
||||||
|
"lib/tls.js",
|
||||||
|
"lib/trace_events.js",
|
||||||
|
"lib/tty.js",
|
||||||
|
"lib/url.js",
|
||||||
|
"lib/util.js",
|
||||||
|
"lib/util/types.js",
|
||||||
|
"lib/v8.js",
|
||||||
|
"lib/vm.js",
|
||||||
|
"lib/wasi.js",
|
||||||
|
"lib/worker_threads.js",
|
||||||
|
"lib/zlib.js"
|
||||||
|
],
|
||||||
|
"node_module_version": 127,
|
||||||
|
"node_no_browser_globals": "false",
|
||||||
|
"node_prefix": "/usr/local",
|
||||||
|
"node_release_urlbase": "https://nodejs.org/download/release/",
|
||||||
|
"node_shared": "false",
|
||||||
|
"node_shared_ada": "false",
|
||||||
|
"node_shared_brotli": "false",
|
||||||
|
"node_shared_cares": "false",
|
||||||
|
"node_shared_http_parser": "false",
|
||||||
|
"node_shared_libuv": "false",
|
||||||
|
"node_shared_nghttp2": "false",
|
||||||
|
"node_shared_nghttp3": "false",
|
||||||
|
"node_shared_ngtcp2": "false",
|
||||||
|
"node_shared_openssl": "false",
|
||||||
|
"node_shared_simdjson": "false",
|
||||||
|
"node_shared_simdutf": "false",
|
||||||
|
"node_shared_sqlite": "false",
|
||||||
|
"node_shared_uvwasi": "false",
|
||||||
|
"node_shared_zlib": "false",
|
||||||
|
"node_shared_zstd": "false",
|
||||||
|
"node_tag": "",
|
||||||
|
"node_target_type": "executable",
|
||||||
|
"node_use_amaro": "true",
|
||||||
|
"node_use_bundled_v8": "true",
|
||||||
|
"node_use_node_code_cache": "true",
|
||||||
|
"node_use_node_snapshot": "true",
|
||||||
|
"node_use_openssl": "true",
|
||||||
|
"node_use_sqlite": "true",
|
||||||
|
"node_use_v8_platform": "true",
|
||||||
|
"node_with_ltcg": "false",
|
||||||
|
"node_without_node_options": "false",
|
||||||
|
"node_write_snapshot_as_array_literals": "false",
|
||||||
|
"openssl_is_fips": "false",
|
||||||
|
"openssl_quic": "false",
|
||||||
|
"ossfuzz": "false",
|
||||||
|
"shlib_suffix": "127.dylib",
|
||||||
|
"single_executable_application": "true",
|
||||||
|
"suppress_all_error_on_warn": "false",
|
||||||
|
"target_arch": "arm64",
|
||||||
|
"ubsan": 0,
|
||||||
|
"use_ccache_win": 0,
|
||||||
|
"use_prefix_to_find_headers": "false",
|
||||||
|
"v8_enable_31bit_smis_on_64bit_arch": 0,
|
||||||
|
"v8_enable_extensible_ro_snapshot": 0,
|
||||||
|
"v8_enable_external_code_space": 0,
|
||||||
|
"v8_enable_gdbjit": 0,
|
||||||
|
"v8_enable_hugepage": 0,
|
||||||
|
"v8_enable_i18n_support": 1,
|
||||||
|
"v8_enable_inspector": 1,
|
||||||
|
"v8_enable_javascript_promise_hooks": 1,
|
||||||
|
"v8_enable_lite_mode": 0,
|
||||||
|
"v8_enable_maglev": 0,
|
||||||
|
"v8_enable_object_print": 1,
|
||||||
|
"v8_enable_pointer_compression": 0,
|
||||||
|
"v8_enable_pointer_compression_shared_cage": 0,
|
||||||
|
"v8_enable_sandbox": 0,
|
||||||
|
"v8_enable_shared_ro_heap": 1,
|
||||||
|
"v8_enable_webassembly": 1,
|
||||||
|
"v8_optimized_debug": 1,
|
||||||
|
"v8_promise_internal_field_count": 1,
|
||||||
|
"v8_random_seed": 0,
|
||||||
|
"v8_trace_maps": 0,
|
||||||
|
"v8_use_siphash": 1,
|
||||||
|
"want_separate_host_toolset": 0,
|
||||||
|
"xcode_version": "16.0",
|
||||||
|
"nodedir": "/var/folders/6f/8vj8fvhd31n00w9ncbf9v4lc0000gn/T/prebuild/node/22.21.0",
|
||||||
|
"python": "/Applications/Xcode.app/Contents/Developer/usr/bin/python3",
|
||||||
|
"standalone_static_library": 1,
|
||||||
|
"target": "22.21.0",
|
||||||
|
"build_v8_with_gn": "false",
|
||||||
|
"global_prefix": "/usr/local",
|
||||||
|
"local_prefix": "/Users/bergautosoft/Documents/projects/fltr-app",
|
||||||
|
"globalconfig": "/usr/local/etc/npmrc",
|
||||||
|
"userconfig": "/Users/bergautosoft/.npmrc",
|
||||||
|
"init_module": "/Users/bergautosoft/.npm-init.js",
|
||||||
|
"npm_version": "10.9.4",
|
||||||
|
"node_gyp": "/usr/local/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js",
|
||||||
|
"cache": "/Users/bergautosoft/.npm",
|
||||||
|
"user_agent": "npm/10.9.4 node/v22.21.0 darwin arm64 workspaces/false",
|
||||||
|
"prefix": "/usr/local"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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={styles.content}>
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
<Text style={styles.personName}>
|
<Text style={styles.personName}>{person.name || "Unbekannt"}</Text>
|
||||||
{person.name || "Unbekannt"}
|
{age != null && <Text style={styles.personMeta}>{age} Jahre</Text>}
|
||||||
{age != null ? ` (${age})` : ""}
|
</View>
|
||||||
</Text>
|
<Feather name="chevron-right" size={16} color="rgba(255,255,255,0.3)" />
|
||||||
{/* <Text style={styles.personMeta}>aus: unterschiedlichen Shows</Text> */}
|
|
||||||
</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.detailTitle}>Beschreibung</Text>
|
||||||
<Text style={styles.detailLabel}>{description}</Text>
|
<Text style={styles.detailLabel}>{description}</Text>
|
||||||
<Text style={styles.detailTitle}>Konzept:</Text>
|
</BlurView>
|
||||||
|
<BlurView intensity={20} tint="dark" style={styles.card}>
|
||||||
|
<Text style={styles.detailTitle}>Konzept</Text>
|
||||||
<Text style={styles.detailLabel}>{concept}</Text>
|
<Text style={styles.detailLabel}>{concept}</Text>
|
||||||
<Text style={styles.detailTitle}>Genres:</Text>
|
</BlurView>
|
||||||
<Text style={styles.detailLabel}>{genres.join(', ')}</Text>
|
<BlurView intensity={20} tint="dark" style={styles.card}>
|
||||||
<Text style={styles.detailTitle}>Produktion:</Text>
|
<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>
|
<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} />
|
||||||
|
|
||||||
|
{/* Streaming service icons */}
|
||||||
|
<View style={styles.serviceRow}>
|
||||||
|
{streamingServicesUris.length > 0 &&
|
||||||
|
streamingServicesUris.map((service) => (
|
||||||
<Image
|
<Image
|
||||||
key={service}
|
key={service}
|
||||||
source={{
|
source={{ uri: service }}
|
||||||
uri: service,
|
style={styles.serviceIcon}
|
||||||
}}
|
|
||||||
style={{ height: 45, width: 45, resizeMode: 'contain', borderRadius: 100}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
))}
|
))}
|
||||||
</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,
|
serviceRow: {
|
||||||
shadowRadius: 1.0,
|
flexDirection: "row",
|
||||||
elevation: 1,
|
justifyContent: "flex-end",
|
||||||
|
padding: 10,
|
||||||
|
gap: 6,
|
||||||
},
|
},
|
||||||
streamingServiceIcon: {
|
serviceIcon: {
|
||||||
width: 45,
|
height: 34,
|
||||||
height: 45,
|
width: 34,
|
||||||
marginLeft: "auto",
|
borderRadius: 17,
|
||||||
marginRight: 15,
|
resizeMode: "contain",
|
||||||
borderWidth: 1,
|
backgroundColor: "rgba(0,0,0,0.3)",
|
||||||
borderColor: "white",
|
|
||||||
borderRadius: 15,
|
|
||||||
marginTop: 15,
|
|
||||||
},
|
},
|
||||||
liveBadgeContainer: {
|
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}>
|
||||||
|
<BlurView intensity={25} tint="dark" style={styles.pill}>
|
||||||
<Text style={styles.showInfoText}>{seasons} Staffeln</Text>
|
<Text style={styles.showInfoText}>{seasons} Staffeln</Text>
|
||||||
<View style={styles.dot} />
|
</BlurView>
|
||||||
|
<BlurView intensity={25} tint="dark" style={styles.pill}>
|
||||||
<Text style={styles.showInfoText}>{participants} Teilnehmer</Text>
|
<Text style={styles.showInfoText}>{participants} Teilnehmer</Text>
|
||||||
<View style={styles.dot} />
|
</BlurView>
|
||||||
|
<BlurView intensity={25} tint="dark" style={styles.pill}>
|
||||||
<Text style={styles.showInfoText}>{streamingService}</Text>
|
<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",
|
|
||||||
shadowOffset: {
|
|
||||||
width: 0,
|
|
||||||
height: 3,
|
|
||||||
},
|
},
|
||||||
shadowOpacity: 0.25,
|
backButton: {
|
||||||
shadowRadius: 3.84,
|
width: 40,
|
||||||
elevation: 5,
|
height: 40,
|
||||||
|
borderRadius: 20,
|
||||||
|
overflow: "hidden",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
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,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
3463
package-lock.json
generated
3463
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
23
package.json
23
package.json
@@ -20,19 +20,19 @@
|
|||||||
"@react-navigation/elements": "^2.3.8",
|
"@react-navigation/elements": "^2.3.8",
|
||||||
"@react-navigation/native": "^7.1.6",
|
"@react-navigation/native": "^7.1.6",
|
||||||
"@tanstack/react-query": "^5.90.5",
|
"@tanstack/react-query": "^5.90.5",
|
||||||
"expo": "54.0.21",
|
"expo": "~54.0.33",
|
||||||
"expo-blur": "~15.0.7",
|
"expo-blur": "~15.0.8",
|
||||||
"expo-constants": "~18.0.10",
|
"expo-constants": "~18.0.10",
|
||||||
"expo-font": "~14.0.8",
|
"expo-font": "~14.0.8",
|
||||||
"expo-haptics": "~15.0.7",
|
"expo-haptics": "~15.0.8",
|
||||||
"expo-image": "~3.0.10",
|
"expo-image": "~3.0.11",
|
||||||
"expo-linking": "~8.0.8",
|
"expo-linking": "~8.0.11",
|
||||||
"expo-router": "~6.0.14",
|
"expo-router": "~6.0.23",
|
||||||
"expo-splash-screen": "~31.0.10",
|
"expo-splash-screen": "~31.0.13",
|
||||||
"expo-status-bar": "~3.0.8",
|
"expo-status-bar": "~3.0.9",
|
||||||
"expo-symbols": "~1.0.7",
|
"expo-symbols": "~1.0.8",
|
||||||
"expo-system-ui": "~6.0.8",
|
"expo-system-ui": "~6.0.9",
|
||||||
"expo-web-browser": "~15.0.8",
|
"expo-web-browser": "~15.0.10",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-native": "0.81.5",
|
"react-native": "0.81.5",
|
||||||
@@ -46,6 +46,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.25.2",
|
"@babel/core": "^7.25.2",
|
||||||
|
"@react-native-community/cli": "^20.0.2",
|
||||||
"@types/react": "~19.1.10",
|
"@types/react": "~19.1.10",
|
||||||
"eslint": "^9.25.0",
|
"eslint": "^9.25.0",
|
||||||
"eslint-config-expo": "~10.0.0",
|
"eslint-config-expo": "~10.0.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