Files
fltr-app/app/(tabs)/home/index.tsx
2026-03-11 13:43:06 +11:00

354 lines
9.2 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
},
});