354 lines
9.2 KiB
TypeScript
354 lines
9.2 KiB
TypeScript
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,
|
||
},
|
||
});
|