Files
fltr-app/app/participant.tsx
2026-03-11 13:43:06 +11:00

295 lines
11 KiB
TypeScript

import { PersonMini } from "@/apis/personHistoryApi";
import styles from "@/app/stackStyles/participantStyles";
import { usePersonHistory } from "@/hooks/usePersonHistory";
import Ionicons from "@expo/vector-icons/Ionicons";
import { router, useLocalSearchParams } from "expo-router";
import * as WebBrowser from "expo-web-browser";
import React from "react";
import {
ActivityIndicator,
Image,
Text,
TouchableOpacity,
View,
} from "react-native";
import {
GestureHandlerRootView,
ScrollView,
} from "react-native-gesture-handler";
export default function ParticipantScreen() {
const { name, participantId, imageUri } = useLocalSearchParams();
const pid = Array.isArray(participantId)
? Number(participantId[0])
: Number(participantId);
const imageUriString = Array.isArray(imageUri) ? imageUri[0] : imageUri;
const isPravatar = imageUriString?.includes("pravatar");
const { data: appearances = [], isLoading, isError } = usePersonHistory(pid);
const formatYear = (iso?: string | null) => {
if (!iso) return null;
const [y] = iso.split("-");
return y || null;
};
const [expandedShows, setExpandedShows] = React.useState<Set<number>>(
new Set(),
);
const toggleExpand = React.useCallback((showId: number) => {
setExpandedShows((prev) => {
const next = new Set(prev);
if (next.has(showId)) next.delete(showId);
else next.add(showId);
return next;
});
}, []);
const goToShow = React.useCallback((id: number) => {
router.push({ pathname: "/showDetails", params: { id: String(id) } });
}, []);
const goToPerson = React.useCallback(
(p: PersonMini) => {
if (!p?.personId) return;
if (p.personId === pid) return;
router.push({
pathname: "/participant",
params: {
participantId: String(p.personId),
name: p.name,
imageUri: p.imageUrl || "",
},
});
},
[pid],
);
return (
<GestureHandlerRootView style={styles.mainContainer}>
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: 40, paddingTop: 100 }}
>
{/* Profile Hero */}
<View style={styles.profileHero}>
<View style={styles.profileImageContainer}>
<Image
source={{ uri: imageUriString || undefined }}
style={styles.profileImage}
resizeMode="cover"
blurRadius={isPravatar ? 16 : 0}
/>
</View>
<Text style={styles.participantName}>{name}</Text>
<Text style={styles.participantSubtitle}>
{appearances.length}{" "}
{appearances.length === 1 ? "Auftritt" : "Auftritte"}
</Text>
<View style={styles.heroButtons}>
<TouchableOpacity
style={styles.searchButton}
onPress={() =>
WebBrowser.openBrowserAsync(
"https://www.google.com/search?udm=2&q=" +
encodeURIComponent(String(name)),
)
}
>
<Ionicons name="images-outline" size={18} color="#199edb" />
<Text style={styles.searchButtonText}>Bilder</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.instagramButton}
onPress={() =>
WebBrowser.openBrowserAsync(
"https://www.google.com/search?q=" +
encodeURIComponent(`${String(name)} Instagram`),
)
}
>
<Ionicons name="logo-instagram" size={18} color="#E1306C" />
<Text style={styles.instagramButtonText}>Instagram</Text>
</TouchableOpacity>
</View>
</View>
{/* Loading */}
{isLoading && (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#199edb" />
</View>
)}
{/* Appearances */}
{!isLoading && appearances.length > 0 && (
<View style={styles.performedShowsSection}>
<Text style={styles.performedShowsTitle}>Auftritte</Text>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={{ flex: 1 }}
contentContainerStyle={{
gap: 20,
paddingHorizontal: 16,
paddingTop: 12,
}}
>
{appearances.toReversed().map(({ show, seasons }) => {
const partners = Array.from(
new Map(
seasons
.map((s) => s.partner)
.filter((p): p is NonNullable<typeof p> => !!p)
.map((p) => [p.personId, p]),
).values(),
);
const allParticipants = Array.from(
new Map(
seasons
.flatMap((s) => s.participants)
.filter((p) => p.personId !== pid)
.map((p) => [p.personId, p]),
).values(),
);
const isExpanded = expandedShows.has(show.id);
const visible = isExpanded
? allParticipants
: allParticipants.slice(0, 12);
const restCount = Math.max(
allParticipants.length - visible.length,
0,
);
return (
<View key={show.id} style={styles.card}>
<TouchableOpacity
style={styles.showContainer}
onPress={() => goToShow(show.id)}
>
<Image
source={{ uri: show.bannerUri || show.thumbnailUri }}
style={styles.showImage}
resizeMode="cover"
/>
</TouchableOpacity>
<View style={styles.cardInfo}>
<View style={styles.cardTitleRow}>
<View style={{ flex: 1 }}>
<Text style={styles.showTitle} numberOfLines={1}>
{show.title}
</Text>
<Text style={styles.showSeason}>
Staffel{" "}
{seasons.map((s) => s.seasonNumber).join(" & ")}
{" · "}
{formatYear(seasons[0]?.startDate)}
</Text>
</View>
<TouchableOpacity
style={styles.cardSearchButton}
onPress={() =>
WebBrowser.openBrowserAsync(
"https://www.google.com/search?udm=2&q=" +
encodeURIComponent(
`${String(name)} ${show.title}`,
),
)
}
>
<Ionicons
name="images-outline"
size={16}
color="#199edb"
/>
</TouchableOpacity>
</View>
{partners.length > 0 && (
<View style={styles.partnerSection}>
<Text style={styles.sectionLabel}>Partner</Text>
<View style={styles.partnerRow}>
{partners.map((p) => (
<TouchableOpacity
key={p.personId}
style={styles.partnerChip}
onPress={() => goToPerson(p)}
>
<Ionicons
name="heart"
size={12}
color="#e74c8b"
/>
<Text
style={styles.partnerChipText}
numberOfLines={1}
>
{p.name}
</Text>
</TouchableOpacity>
))}
</View>
</View>
)}
{allParticipants.length > 0 && (
<View style={styles.participantsSection}>
<Text style={styles.sectionLabel}>
Weitere Teilnehmer
</Text>
<View style={styles.participantRow}>
{visible.map((p) => (
<TouchableOpacity
key={p.personId}
style={styles.participantChip}
onPress={() => goToPerson(p)}
>
<Text
style={styles.participantChipText}
numberOfLines={1}
>
{p.name}
</Text>
</TouchableOpacity>
))}
{!isExpanded && restCount > 0 && (
<TouchableOpacity
onPress={() => toggleExpand(show.id)}
style={styles.moreChip}
>
<Text style={styles.moreChipText}>
+{restCount} mehr
</Text>
</TouchableOpacity>
)}
{isExpanded && allParticipants.length > 12 && (
<TouchableOpacity
onPress={() => toggleExpand(show.id)}
style={styles.moreChip}
>
<Text style={styles.moreChipText}>Weniger</Text>
</TouchableOpacity>
)}
</View>
</View>
)}
</View>
</View>
);
})}
</ScrollView>
</View>
)}
</ScrollView>
</GestureHandlerRootView>
);
}