search
This commit is contained in:
94
components/discovery/SeasonCarousel.tsx
Normal file
94
components/discovery/SeasonCarousel.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
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 },
|
||||
});
|
||||
Reference in New Issue
Block a user