modified: files to ios26 ui/ux

This commit is contained in:
Yordan Simeonov
2026-03-11 13:43:06 +11:00
parent 44e3558681
commit c67e60a57b
23 changed files with 2310 additions and 1618 deletions

353
app/(tabs)/home/index.tsx Normal file
View 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,
},
});