Files
fltr-app/components/discovery/SeasonCarousel.tsx
DevOFVictory f21f20a4fd search
2025-10-23 17:58:16 +02:00

95 lines
3.7 KiB
TypeScript

import { Season, Show } from "@/app/types";
import { FontAwesome } from "@expo/vector-icons";
import React from "react";
import { Dimensions, FlatList, LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent, Pressable, StyleSheet, View } from "react-native";
const WINDOW_WIDTH = Dimensions.get("window").width;
function clamp(n: number, min: number, max: number) { return Math.max(min, Math.min(max, n)); }
export default function SeasonCarousel({
show,
seasons,
renderItem,
}: {
show: Show;
seasons: Season[];
renderItem: (season: Season) => React.ReactNode;
}) {
const [currentIndex, setCurrentIndex] = React.useState(0);
const [sliderWidth, setSliderWidth] = React.useState(Math.floor(WINDOW_WIDTH - 20));
const listRef = React.useRef<FlatList<Season> | null>(null);
const onLayout = (e: LayoutChangeEvent) => {
const w = Math.max(0, Math.floor(e.nativeEvent.layout.width));
if (w) setSliderWidth(w);
};
const onMomentumEnd = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
const x = e.nativeEvent.contentOffset.x;
const index = clamp(Math.round(x / sliderWidth), 0, Math.max(0, seasons.length - 1));
setCurrentIndex(index);
};
const scrollTo = (target: number) => {
const ref = listRef.current;
if (!ref) return;
try { ref.scrollToIndex({ index: target, animated: true }); } catch {}
};
const goPrev = () => {
setCurrentIndex((curr) => { const next = clamp(curr - 1, 0, Math.max(0, seasons.length - 1)); if (next !== curr) setTimeout(() => scrollTo(next), 0); return next; });
};
const goNext = () => {
setCurrentIndex((curr) => { const next = clamp(curr + 1, 0, Math.max(0, seasons.length - 1)); if (next !== curr) setTimeout(() => scrollTo(next), 0); return next; });
};
return (
<View style={{ marginBottom: 10, backgroundColor: "#1b1e2b", borderRadius: 10 }} onLayout={onLayout}>
<FlatList
ref={(r) => (listRef.current = r)}
data={seasons}
keyExtractor={(season, idx) => `${show.showId}-${(season as any)?.seasonId ?? `season-${idx}`}`}
horizontal
pagingEnabled
showsHorizontalScrollIndicator={false}
snapToAlignment="start"
decelerationRate="fast"
onMomentumScrollEnd={onMomentumEnd}
renderItem={({ item }) => (
<View style={{ width: sliderWidth }}>
{renderItem(item)}
</View>
)}
/>
{seasons.length > 1 && (
<View style={carouselStyles.controls}>
<Pressable onPress={goPrev} style={[carouselStyles.arrowButton, currentIndex <= 0 && carouselStyles.arrowDisabled]} disabled={currentIndex <= 0} hitSlop={8}>
<FontAwesome name="chevron-left" size={16} color="#bbb" />
</Pressable>
<View style={carouselStyles.dotsRow}>
{seasons.map((_, i) => (
<View key={`dot-${show.showId}-${i}`} style={[carouselStyles.dot, i === currentIndex && carouselStyles.dotActive]} />
))}
</View>
<Pressable onPress={goNext} style={[carouselStyles.arrowButton, currentIndex >= seasons.length - 1 && carouselStyles.arrowDisabled]} disabled={currentIndex >= seasons.length - 1} hitSlop={8}>
<FontAwesome name="chevron-right" size={16} color="#bbb" />
</Pressable>
</View>
)}
</View>
);
}
const carouselStyles = StyleSheet.create({
controls: { paddingHorizontal: 8, width: "100%", flexDirection: "row", alignItems: "center", justifyContent: "space-between" },
dotsRow: { flexDirection: "row", alignItems: "center" },
dot: { width: 6, height: 6, borderRadius: 999, backgroundColor: "#888", opacity: 0.4, marginHorizontal: 3 },
dotActive: { opacity: 1 },
arrowButton: { padding: 6, opacity: 0.9 },
arrowDisabled: { opacity: 0.3 },
});