This commit is contained in:
DevOFVictory
2025-10-23 17:58:16 +02:00
parent 52f2e241a7
commit f21f20a4fd
9 changed files with 566 additions and 108 deletions

View File

@@ -0,0 +1,19 @@
import { StyleSheet, Text, TextProps } from 'react-native';
export default function GenreTag(props: TextProps) {
return <Text {...props} style={[props.style, styles.genreTag]} />;
}
const styles = StyleSheet.create({
genreTag: {
fontFamily: 'SpaceMono',
fontSize: 12,
paddingVertical: 4,
paddingHorizontal: 8,
borderRadius: 12,
backgroundColor: '#333747',
color: '#fff',
textAlign: 'center',
overflow: 'hidden',
},
});

View File

@@ -0,0 +1,49 @@
import { FontAwesome } from "@expo/vector-icons";
import { useNavigation } from "@react-navigation/native";
import React from "react";
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
export type PersonLite = { id?: number; personId?: number; name?: string; birthDate?: string | null; imageUrl?: string | null };
const calcAge = (birthDate?: string | null): number | null => {
if (!birthDate) return null;
const d = new Date(birthDate);
if (isNaN(d.getTime())) return null;
const today = new Date();
let age = today.getFullYear() - d.getFullYear();
const m = today.getMonth() - d.getMonth();
if (m < 0 || (m === 0 && today.getDate() < d.getDate())) age--;
return age < 0 || age > 130 ? null : age;
};
export default function PersonRow({ person }: { person: PersonLite }) {
const navigation = useNavigation();
const age = calcAge(person.birthDate);
const id = person.personId ?? person.id;
return (
<TouchableOpacity
onPress={() => {
// If your PersonDetail expects a Person object instead of an id, adapt this accordingly
// navigation.navigate("PersonDetail" as never, { personId: id } as never);
}}
style={styles.personRow}
>
<View style={styles.avatarCircle}>
<FontAwesome name="user" size={22} color="#ccc" />
</View>
<View style={{ flex: 1 }}>
<Text style={styles.personName}>{person.name || "Unbekannt"}{age != null ? ` (${age})` : ""}</Text>
<Text style={styles.personMeta}>aus: unterschiedlichen Shows</Text>
</View>
<FontAwesome name="chevron-right" size={14} color="#888" />
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
personRow: { width: "100%", flexDirection: "row", alignItems: "center", backgroundColor: "#1b1e2b", borderRadius: 10, paddingHorizontal: 10, paddingVertical: 10, marginBottom: 8 },
avatarCircle: { width: 40, height: 40, borderRadius: 999, backgroundColor: "#2a2f45", alignItems: "center", justifyContent: "center", marginRight: 10 },
personName: { color: "white", fontSize: 16, fontWeight: "600" },
personMeta: { color: "#bbb", fontSize: 12, marginTop: 2 },
});

View 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 },
});

View File

@@ -0,0 +1,123 @@
import { Season, Show } from "@/app/types";
import GenreTag from "@/components/discovery/GenreTag";
import { useNavigation } from "@react-navigation/native";
import { Image, StyleSheet, Text, TouchableOpacity, View } from "react-native";
export default function ShowBox({
show,
displayedSeason,
shadow = true,
}: {
show: Show;
displayedSeason?: Season;
shadow?: boolean;
}) {
const navigation = useNavigation();
return (
<TouchableOpacity
onPress={() => navigation.navigate("ShowDetail", { show })}
style={
!shadow
? [styles.showContainer, { backgroundColor: "#1b1e2b", paddingBottom: 0 }]
: [styles.showContainer, styles.shadow, { backgroundColor: "#1b1e2b" }]
}
>
<View style={styles.showImageContainer}>
<Image source={{ uri: show.thumbnailUrl }} style={styles.showImage} />
{show.running && <Text style={styles.runningTag}>LIVE</Text>}
</View>
<View style={styles.showRight}>
<Text style={styles.showTitle}>{show.title}</Text>
{displayedSeason ? (
<Text style={{ fontWeight: "bold", color: "#aac0ce" }}>
Staffel {displayedSeason.seasonNumber} (
{new Date(displayedSeason.startDate).getFullYear()})
</Text>
) : null}
<Text style={styles.showDescription} numberOfLines={8} ellipsizeMode="tail">
{show.description}
</Text>
<View style={styles.showGenreTagContainer}>
{show.genre.split(", ").map((genre: any) => (
<GenreTag key={genre}>{genre}</GenreTag>
))}
</View>
</View>
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
showTitle: {
fontSize: 18,
fontWeight: "bold",
textAlign: "left",
color: "#ffffff",
},
showDescription: {
marginTop: 5,
fontSize: 12,
textAlign: "left",
flex: 1,
color: "#cccccc",
},
showContainer: {
width: "100%",
height: 220,
alignItems: "center",
padding: 10,
borderRadius: 10,
flexDirection: "row",
justifyContent: "flex-start",
backgroundColor: "#1b1e2b",
},
shadow: {
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 3.84,
elevation: 5,
},
showImageContainer: {
width: 140,
height: "100%",
backgroundColor: "#2b2e3b",
borderRadius: 10,
},
showImage: {
width: "100%",
height: "100%",
borderRadius: 10,
},
showRight: {
flex: 1,
height: "100%",
flexDirection: "column",
backgroundColor: "#1b1e2b",
paddingLeft: 10,
paddingVertical: 2,
},
showGenreTagContainer: {
flexDirection: "row",
justifyContent: "flex-start",
flexWrap: "wrap",
gap: 5,
marginTop: 2,
backgroundColor: "#1b1e2b",
},
runningTag: {
position: "absolute",
top: 3,
left: 3,
backgroundColor: "red",
opacity: 0.65,
color: "white",
padding: 5,
borderRadius: 90,
},
});

View File

@@ -0,0 +1,20 @@
import { FontAwesome } from "@expo/vector-icons";
import React from "react";
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
export default function TagChip({ icon, label, onPress }: { icon: any; label: string; onPress: () => void }) {
return (
<TouchableOpacity onPress={onPress}>
<View style={styles.tag}>
<FontAwesome name={icon} size={16} color="#bbb" style={{ marginRight: 6 }} />
<Text style={styles.tagLabel}>{label}</Text>
<FontAwesome name="times-circle" size={16} color="#bbb" style={{ marginLeft: 6 }} />
</View>
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
tag: { flexDirection: "row", alignItems: "center", backgroundColor: "#333", borderRadius: 999, paddingHorizontal: 10, paddingVertical: 6, marginRight: 8, marginBottom: 8 },
tagLabel: { color: "white" },
});