ready to release
2
app.json
@@ -33,7 +33,7 @@
|
|||||||
"image": "./assets/images/icon.png",
|
"image": "./assets/images/icon.png",
|
||||||
"imageWidth": 300,
|
"imageWidth": 300,
|
||||||
"resizeMode": "contain",
|
"resizeMode": "contain",
|
||||||
"backgroundColor": "#000456"
|
"backgroundColor": "#000457"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"expo-web-browser"
|
"expo-web-browser"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { AutoCompleteItem } from "@/apis/autoCompleteApi";
|
import { AutoCompleteItem } from "@/apis/autoCompleteApi";
|
||||||
import { SearchResultItem } from "@/apis/searchApi";
|
|
||||||
import { Season } from "@/apis/seasonApi";
|
import { Season } from "@/apis/seasonApi";
|
||||||
import { Show } from "@/apis/showApi";
|
import { Show } from "@/apis/showApi";
|
||||||
import styles from "@/app/tabStyles/indexStyles";
|
import styles from "@/app/tabStyles/indexStyles";
|
||||||
@@ -8,33 +7,52 @@ import { useDiscoveryContext } from "@/contexts/DiscoveryContext";
|
|||||||
import { FontAwesome } from "@expo/vector-icons";
|
import { FontAwesome } from "@expo/vector-icons";
|
||||||
import Feather from "@expo/vector-icons/Feather";
|
import Feather from "@expo/vector-icons/Feather";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Keyboard, ScrollView, Text, TextInput, TouchableOpacity, View } from "react-native";
|
import {
|
||||||
|
Keyboard,
|
||||||
|
ScrollView,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
TouchableOpacity,
|
||||||
|
TouchableWithoutFeedback,
|
||||||
|
View,
|
||||||
|
} from "react-native";
|
||||||
|
|
||||||
import { useSearch } from "@/hooks/useSearch";
|
|
||||||
import { getShowById } from "@/apis/showApi";
|
import { getShowById } from "@/apis/showApi";
|
||||||
import PersonRow from "@/components/discovery/PersonRow";
|
import PersonRow from "@/components/discovery/PersonRow";
|
||||||
import SeasonCarousel from "@/components/discovery/SeasonCarousel";
|
import SeasonCarousel from "@/components/discovery/SeasonCarousel";
|
||||||
import TagChip from "@/components/discovery/TagChip";
|
import TagChip from "@/components/discovery/TagChip";
|
||||||
import { getIconName, mapApiPersonToUI, mapApiSeasonToUI, mapApiShowToUI } from "@/utils/searchMapping";
|
import { useSearch } from "@/hooks/useSearch";
|
||||||
|
import {
|
||||||
|
getIconName,
|
||||||
|
mapApiPersonToUI,
|
||||||
|
mapApiSeasonToUI,
|
||||||
|
mapApiShowToUI,
|
||||||
|
} from "@/utils/searchMapping";
|
||||||
|
|
||||||
export default function ExploreScreen() {
|
export default function ExploreScreen() {
|
||||||
const { query, setQuery, suggestions } = useDiscoveryContext();
|
const { query, setQuery, suggestions } = useDiscoveryContext();
|
||||||
|
|
||||||
const [tags, setTags] = React.useState<AutoCompleteItem[]>([]);
|
const [tags, setTags] = React.useState<AutoCompleteItem[]>([]);
|
||||||
const tagStrings = React.useMemo(() => tags.map((t) => t.text), [tags]);
|
const tagStrings = React.useMemo(() => tags.map((t) => t.text), [tags]);
|
||||||
const { data: results = [] } = useSearch(tagStrings);
|
|
||||||
|
|
||||||
// Show metadata cache by id (filled from SHOW results and lazy-loaded by id)
|
const {
|
||||||
|
data: results = [],
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
// isLoading, // optional, falls benötigt
|
||||||
|
} = useSearch(tagStrings as string[]);
|
||||||
|
|
||||||
|
// Lokaler Show-Cache
|
||||||
const [showsById, setShowsById] = React.useState<Record<number, Show>>({});
|
const [showsById, setShowsById] = React.useState<Record<number, Show>>({});
|
||||||
|
|
||||||
// --- helpers ---
|
// Steuerung für Vorschlagsliste
|
||||||
|
const [showSuggestions, setShowSuggestions] = React.useState(false);
|
||||||
|
|
||||||
function tagAdded(tag: AutoCompleteItem) {
|
function tagAdded(tag: AutoCompleteItem) {
|
||||||
const nextTags = tags.some((t) => t.text === tag.text) ? tags : [...tags, tag];
|
const nextTags = tags.some((t) => t.text === tag.text) ? tags : [...tags, tag];
|
||||||
setTags(nextTags);
|
setTags(nextTags);
|
||||||
|
|
||||||
setQuery("");
|
setQuery("");
|
||||||
|
setShowSuggestions(false);
|
||||||
Keyboard.dismiss();
|
Keyboard.dismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +61,7 @@ export default function ExploreScreen() {
|
|||||||
setTags(nextTags);
|
setTags(nextTags);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep our local show cache in sync with SHOW items returned by search
|
// Cache mit SHOW-Resultaten füllen
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const fromResults: Record<number, Show> = {};
|
const fromResults: Record<number, Show> = {};
|
||||||
for (const r of results) {
|
for (const r of results) {
|
||||||
@@ -57,9 +75,7 @@ export default function ExploreScreen() {
|
|||||||
}
|
}
|
||||||
}, [results]);
|
}, [results]);
|
||||||
|
|
||||||
|
// SEASON-Ergebnisse nach showId gruppieren
|
||||||
|
|
||||||
// Group SEASON results by showId
|
|
||||||
const seasonsByShowId = React.useMemo(() => {
|
const seasonsByShowId = React.useMemo(() => {
|
||||||
const map = new Map<number, Season[]>();
|
const map = new Map<number, Season[]>();
|
||||||
for (const r of results) {
|
for (const r of results) {
|
||||||
@@ -71,7 +87,6 @@ export default function ExploreScreen() {
|
|||||||
list.push(s as Season);
|
list.push(s as Season);
|
||||||
map.set(key, list);
|
map.set(key, list);
|
||||||
}
|
}
|
||||||
// sort seasons per show by startDate asc
|
|
||||||
for (const [k, list] of map) {
|
for (const [k, list] of map) {
|
||||||
list.sort((a, b) => {
|
list.sort((a, b) => {
|
||||||
const da = a?.startDate ? new Date(a.startDate).getTime() : Number.POSITIVE_INFINITY;
|
const da = a?.startDate ? new Date(a.startDate).getTime() : Number.POSITIVE_INFINITY;
|
||||||
@@ -83,7 +98,7 @@ export default function ExploreScreen() {
|
|||||||
return map;
|
return map;
|
||||||
}, [results]);
|
}, [results]);
|
||||||
|
|
||||||
// Lazy fetch missing shows needed for Season carousels
|
// Fehlende Shows für Carousels nachladen
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const needed = Array.from(seasonsByShowId.keys()).filter((id) => !showsById[id]);
|
const needed = Array.from(seasonsByShowId.keys()).filter((id) => !showsById[id]);
|
||||||
if (needed.length === 0) return;
|
if (needed.length === 0) return;
|
||||||
@@ -97,7 +112,7 @@ export default function ExploreScreen() {
|
|||||||
const next: Record<number, Show> = {};
|
const next: Record<number, Show> = {};
|
||||||
for (const s of fetched) {
|
for (const s of fetched) {
|
||||||
if (!s) continue;
|
if (!s) continue;
|
||||||
next[s.id] = s; // wichtig: s.id, nicht s.showId
|
next[s.id] = s;
|
||||||
}
|
}
|
||||||
if (Object.keys(next).length) {
|
if (Object.keys(next).length) {
|
||||||
setShowsById((prev) => ({ ...prev, ...next }));
|
setShowsById((prev) => ({ ...prev, ...next }));
|
||||||
@@ -107,10 +122,12 @@ export default function ExploreScreen() {
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return () => { cancelled = true; };
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
}, [seasonsByShowId, showsById]);
|
}, [seasonsByShowId, showsById]);
|
||||||
|
|
||||||
// PERSON hits shown at the top (like old screen)
|
// PERSON-Resultate
|
||||||
const persons = React.useMemo(() => {
|
const persons = React.useMemo(() => {
|
||||||
return results
|
return results
|
||||||
.filter((r) => r.type === "PERSON")
|
.filter((r) => r.type === "PERSON")
|
||||||
@@ -118,22 +135,97 @@ export default function ExploreScreen() {
|
|||||||
.sort((a, b) => (a.name || "").localeCompare(b.name || ""));
|
.sort((a, b) => (a.name || "").localeCompare(b.name || ""));
|
||||||
}, [results]);
|
}, [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 (
|
return (
|
||||||
<View style={[styles.mainContainer]}>
|
<View style={[styles.mainContainer]}>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<Text style={[styles.title, { fontSize: 28 }]}>Durchsuchen</Text>
|
<Text style={[styles.title, { fontSize: 28 }]}>Durchsuchen</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
<TouchableWithoutFeedback
|
||||||
|
onPress={() => {
|
||||||
|
Keyboard.dismiss();
|
||||||
|
setShowSuggestions(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
<View style={styles.sectionContainer}>
|
<View style={styles.sectionContainer}>
|
||||||
{/* Search bar */}
|
|
||||||
<View style={styles.searchContainer}>
|
<View style={styles.searchContainer}>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={query}
|
value={query}
|
||||||
onChangeText={setQuery}
|
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?"
|
placeholder="Wonach suchst du?"
|
||||||
placeholderTextColor=""
|
placeholderTextColor=""
|
||||||
style={styles.searchInput}
|
style={styles.searchInput}
|
||||||
returnKeyType="search"
|
returnKeyType="search"
|
||||||
|
onFocus={() => setShowSuggestions(true)}
|
||||||
onSubmitEditing={() => {
|
onSubmitEditing={() => {
|
||||||
if (!query.trim()) return;
|
if (!query.trim()) return;
|
||||||
tagAdded({ type: "CUSTOM", text: query.trim() });
|
tagAdded({ type: "CUSTOM", text: query.trim() });
|
||||||
@@ -144,11 +236,18 @@ export default function ExploreScreen() {
|
|||||||
{query.length === 0 ? (
|
{query.length === 0 ? (
|
||||||
<Feather name="search" size={24} color="hsl(221, 39%, 80%)" />
|
<Feather name="search" size={24} color="hsl(221, 39%, 80%)" />
|
||||||
) : (
|
) : (
|
||||||
<Feather name="x" size={24} color="hsl(221, 39%, 80%)" onPress={() => setQuery("")} />
|
<Feather
|
||||||
|
name="x"
|
||||||
|
size={24}
|
||||||
|
color="hsl(221, 39%, 80%)"
|
||||||
|
onPress={() => {
|
||||||
|
setQuery("");
|
||||||
|
setShowSuggestions(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Tag chips */}
|
|
||||||
<View style={styles.tagContainer}>
|
<View style={styles.tagContainer}>
|
||||||
{tags.map((tag) => (
|
{tags.map((tag) => (
|
||||||
<TagChip
|
<TagChip
|
||||||
@@ -157,14 +256,13 @@ export default function ExploreScreen() {
|
|||||||
label={tag.text}
|
label={tag.text}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
tagRemoved(tag);
|
tagRemoved(tag);
|
||||||
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Suggestions dropdown */}
|
{/* Suggestions dropdown */}
|
||||||
{query.length > 0 && (
|
{query.length > 0 && showSuggestions && (
|
||||||
<View style={styles.suggestionsSection}>
|
<View style={styles.suggestionsSection}>
|
||||||
<Text style={styles.suggestionTitle}>Suchvorschläge</Text>
|
<Text style={styles.suggestionTitle}>Suchvorschläge</Text>
|
||||||
<ScrollView keyboardShouldPersistTaps="handled">
|
<ScrollView keyboardShouldPersistTaps="handled">
|
||||||
@@ -174,8 +272,14 @@ export default function ExploreScreen() {
|
|||||||
style={styles.suggestionContainer}
|
style={styles.suggestionContainer}
|
||||||
onPress={() => tagAdded(suggestion)}
|
onPress={() => tagAdded(suggestion)}
|
||||||
>
|
>
|
||||||
<FontAwesome name={getIconName(suggestion.type)} size={16} color="hsl(0, 0%, 90%)" />
|
<FontAwesome
|
||||||
<Text style={styles.suggestionLabel}>{suggestion.text}</Text>
|
name={getIconName(suggestion.type)}
|
||||||
|
size={16}
|
||||||
|
color="hsl(0, 0%, 90%)"
|
||||||
|
/>
|
||||||
|
<Text style={styles.suggestionLabel}>
|
||||||
|
{suggestion.text}
|
||||||
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
@@ -196,21 +300,46 @@ export default function ExploreScreen() {
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Staffeln grouped by Show with page view */}
|
|
||||||
<View style={styles.sectionContainer}>
|
<View style={styles.sectionContainer}>
|
||||||
|
{seasonsByShowId.size > 0 && (
|
||||||
<Text style={styles.sectionTitle}>Staffeln</Text>
|
<Text style={styles.sectionTitle}>Staffeln</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
{Array.from(seasonsByShowId.entries()).map(([showId, seasons]) => {
|
{Array.from(seasonsByShowId.entries()).map(([showId, seasons]) => {
|
||||||
const show = showsById[Number(showId)];
|
const show = showsById[Number(showId)];
|
||||||
if (!seasons || seasons.length === 0) return null;
|
if (!seasons || seasons.length === 0) return null;
|
||||||
// If show metadata is not yet loaded, render a minimal ShowBox fallback once per page item
|
|
||||||
if (!show) {
|
if (!show) {
|
||||||
return (
|
return (
|
||||||
<SeasonCarousel
|
<SeasonCarousel
|
||||||
key={`sc-${showId}`}
|
key={`sc-${showId}`}
|
||||||
show={{ showId: showId as any, title: "blaaa", description: "", genre: "", thumbnailUrl: "", running: false } as any}
|
show={
|
||||||
|
{
|
||||||
|
showId: showId as any,
|
||||||
|
title: "blaaa",
|
||||||
|
description: "",
|
||||||
|
genre: "",
|
||||||
|
thumbnailUrl: "",
|
||||||
|
running: false,
|
||||||
|
} as any
|
||||||
|
}
|
||||||
seasons={seasons}
|
seasons={seasons}
|
||||||
renderItem={(s) => <ShowBox show={{ showId: showId as any, title: `Show #${showId}`, description: "", genre: "", thumbnailUrl: "", running: false } as any} displayedSeason={s} shadow={false} />}
|
renderItem={(s) => (
|
||||||
|
<ShowBox
|
||||||
|
show={
|
||||||
|
{
|
||||||
|
showId: showId as any,
|
||||||
|
title: `Show #${showId}`,
|
||||||
|
description: "",
|
||||||
|
genre: "",
|
||||||
|
thumbnailUrl: "",
|
||||||
|
running: false,
|
||||||
|
} as any
|
||||||
|
}
|
||||||
|
displayedSeason={s}
|
||||||
|
shadow={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -219,19 +348,96 @@ export default function ExploreScreen() {
|
|||||||
key={`sc-${showId}`}
|
key={`sc-${showId}`}
|
||||||
show={show}
|
show={show}
|
||||||
seasons={seasons}
|
seasons={seasons}
|
||||||
renderItem={(s) => <ShowBox show={show} displayedSeason={s} shadow={false} />}
|
renderItem={(s) => (
|
||||||
|
<ShowBox show={show} displayedSeason={s} shadow={false} />
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{seasonsByShowId.size === 0 && (
|
{/* Schöner Empty-State */}
|
||||||
<Text style={styles.centerText}>
|
{noResults && (
|
||||||
Keine Staffeln gefunden. Passe deine Tags an.
|
<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>
|
||||||
|
<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>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import styles from "@/app/tabStyles/indexStyles";
|
|||||||
import ShowCard from "@/components/ui/ShowCard";
|
import ShowCard from "@/components/ui/ShowCard";
|
||||||
import { useShows } from "@/hooks/useShows";
|
import { useShows } from "@/hooks/useShows";
|
||||||
import { useStreamingServices } from "@/hooks/useStreamingServices";
|
import { useStreamingServices } from "@/hooks/useStreamingServices";
|
||||||
|
import Feather from "@expo/vector-icons/Feather";
|
||||||
import * as Haptics from "expo-haptics";
|
import * as Haptics from "expo-haptics";
|
||||||
import { router } from "expo-router";
|
import { router } from "expo-router";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@@ -89,6 +90,28 @@ export default function HomeScreen() {
|
|||||||
<GestureHandlerRootView>
|
<GestureHandlerRootView>
|
||||||
<View style={styles.mainContainer}>
|
<View style={styles.mainContainer}>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => {
|
||||||
|
router.push("/legal");
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 16,
|
||||||
|
top: "63%",
|
||||||
|
transform: [{ translateY: -12 }],
|
||||||
|
height: 40,
|
||||||
|
width: 40,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
borderRadius: 10,
|
||||||
|
backgroundColor: "rgba(255,255,255,0.06)",
|
||||||
|
}}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Menü öffnen"
|
||||||
|
>
|
||||||
|
<Feather name="menu" size={22} color="#FFFFFF" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
<Text style={styles.title}>FLTR</Text>
|
<Text style={styles.title}>FLTR</Text>
|
||||||
</View>
|
</View>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
|
|||||||
@@ -23,7 +23,14 @@ export default function RootLayout() {
|
|||||||
headerShown: false,
|
headerShown: false,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="legal"
|
||||||
|
options={{
|
||||||
|
presentation: "modal",
|
||||||
|
headerShown: false
|
||||||
|
}} />
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
</DiscoveryProvider>
|
</DiscoveryProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
154
app/legal.tsx
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import Feather from "@expo/vector-icons/Feather";
|
||||||
|
import { router } from "expo-router";
|
||||||
|
import React from "react";
|
||||||
|
import { ScrollView, Text, TouchableOpacity, View } from "react-native";
|
||||||
|
|
||||||
|
export default function LegalScreen() {
|
||||||
|
return (
|
||||||
|
<View style={{ flex: 1, backgroundColor: "hsl(220, 15%, 10%)" }}>
|
||||||
|
|
||||||
|
|
||||||
|
<View
|
||||||
|
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
|
||||||
|
onPress={() => router.back()}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Modal schließen"
|
||||||
|
style={{
|
||||||
|
height: 40,
|
||||||
|
width: 40,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
borderRadius: 10,
|
||||||
|
backgroundColor: "rgba(255,255,255,0.08)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Feather name="x" size={22} color="#FFFFFF" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
contentContainerStyle={{
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingBottom: 28,
|
||||||
|
}}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
{/* Impressum Card */}
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
backgroundColor: "rgba(255,255,255,0.05)",
|
||||||
|
borderRadius: 14,
|
||||||
|
padding: 16,
|
||||||
|
gap: 10,
|
||||||
|
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 style={{ height: 8 }} />
|
||||||
|
|
||||||
|
<View style={{ gap: 4 }}>
|
||||||
|
<Text style={styles.dim}>+49 1522 5642948</Text>
|
||||||
|
<Text style={styles.dim}>kontakt@berg-autosoft.de</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={{ height: 8 }} />
|
||||||
|
|
||||||
|
<View style={{ gap: 4 }}>
|
||||||
|
<Text style={styles.dim}>Steuernummer: 25/103/17193</Text>
|
||||||
|
<Text style={styles.dim}>USt-ID: DE361689728</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Support Card */}
|
||||||
|
<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}>
|
||||||
|
Sollten Sie Probleme bei der Nutzung der iOS- oder Android-App FLTR
|
||||||
|
haben, wenden Sie sich bitte direkt an den Support.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<View style={{ height: 6 }} />
|
||||||
|
|
||||||
|
<Text style={styles.body}>Schreiben Sie eine E-Mail an:</Text>
|
||||||
|
<Text style={[styles.mono, { fontSize: 15 }]}>
|
||||||
|
developer@berg-autosoft.de
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text style={[styles.dim, { marginTop: 10 }]}>
|
||||||
|
Wir bemühen uns, Ihr Anliegen so schnell wie möglich zu bearbeiten.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<View style={{ alignItems: "center", marginTop: 18 }}>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: "rgba(255,255,255,0.6)",
|
||||||
|
fontSize: 12,
|
||||||
|
letterSpacing: 0.2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
© 2025 Berg Autosoft
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
mono: {
|
||||||
|
color: "rgba(255,255,255,0.92)",
|
||||||
|
fontSize: 16,
|
||||||
|
} as const,
|
||||||
|
dim: {
|
||||||
|
color: "rgba(255,255,255,0.75)",
|
||||||
|
fontSize: 14,
|
||||||
|
} as const,
|
||||||
|
body: {
|
||||||
|
color: "rgba(255,255,255,0.88)",
|
||||||
|
fontSize: 14,
|
||||||
|
lineHeight: 20,
|
||||||
|
} as const,
|
||||||
|
};
|
||||||
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 17 KiB |