96 lines
3.7 KiB
TypeScript
96 lines
3.7 KiB
TypeScript
import { Season } from "@/apis/seasonApi";
|
|
import { Show } from "@/apis/showApi";
|
|
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);
|
|
|
|
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={listRef}
|
|
data={seasons}
|
|
keyExtractor={(season, idx) => `${show.id}-${(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.id}-${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 },
|
|
});
|