search
This commit is contained in:
19
components/discovery/GenreTag.tsx
Normal file
19
components/discovery/GenreTag.tsx
Normal 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',
|
||||
},
|
||||
});
|
||||
49
components/discovery/PersonRow.tsx
Normal file
49
components/discovery/PersonRow.tsx
Normal 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 },
|
||||
});
|
||||
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 },
|
||||
});
|
||||
123
components/discovery/ShowBox.tsx
Normal file
123
components/discovery/ShowBox.tsx
Normal 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,
|
||||
},
|
||||
});
|
||||
20
components/discovery/TagChip.tsx
Normal file
20
components/discovery/TagChip.tsx
Normal 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" },
|
||||
});
|
||||
Reference in New Issue
Block a user