modified: files to ios26 ui/ux
This commit is contained in:
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,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user