349 lines
11 KiB
TypeScript
349 lines
11 KiB
TypeScript
import styles from "@/app/stackStyles/participantStyles";
|
|
import { useShowContext } from "@/contexts/ShowContext";
|
|
import Ionicons from "@expo/vector-icons/Ionicons";
|
|
import { router, useLocalSearchParams } from "expo-router";
|
|
import React from "react";
|
|
import { Text, TouchableOpacity, View, Image, Dimensions } from "react-native";
|
|
import {
|
|
getPersonHistory,
|
|
type PersonMini,
|
|
type PersonHistoryRecord,
|
|
} from "@/apis/personHistoryApi";
|
|
import { getShowById } from "@/apis/showApi";
|
|
import {
|
|
GestureHandlerRootView,
|
|
ScrollView,
|
|
} from "react-native-gesture-handler";
|
|
|
|
type SeasonEntry = {
|
|
seasonNumber: number;
|
|
partner: PersonMini | null;
|
|
participants: PersonMini[];
|
|
startDate: string | null;
|
|
};
|
|
|
|
type AppearanceGroup = {
|
|
show: {
|
|
id: number;
|
|
title: string;
|
|
bannerUri: string;
|
|
thumbnailUri: string;
|
|
};
|
|
seasons: SeasonEntry[];
|
|
};
|
|
|
|
export default function ParticipantScreen() {
|
|
const { shows } = useShowContext();
|
|
const { name, participantId } = useLocalSearchParams();
|
|
|
|
const pid = Array.isArray(participantId)
|
|
? Number(participantId[0])
|
|
: Number(participantId);
|
|
|
|
const [loading, setLoading] = React.useState(false);
|
|
const [error, setError] = React.useState<string | null>(null);
|
|
|
|
const [appearances, setAppearances] = React.useState<AppearanceGroup[]>([]);
|
|
|
|
const formatYear = (iso?: string | null) => {
|
|
if (!iso) return null;
|
|
const [y] = iso.split("-");
|
|
return y || null;
|
|
};
|
|
|
|
React.useEffect(() => {
|
|
if (!pid || Number.isNaN(pid)) return;
|
|
const controller = new AbortController();
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
(async () => {
|
|
try {
|
|
const hist = await getPersonHistory(pid, controller.signal);
|
|
|
|
const grouped = new Map<number, Map<number, SeasonEntry>>();
|
|
for (const h of hist) {
|
|
if (!Number.isFinite(h.showId) || h.showId <= 0) continue;
|
|
const seasonsForShow =
|
|
grouped.get(h.showId) ?? new Map<number, SeasonEntry>();
|
|
|
|
const existing = seasonsForShow.get(h.seasonNumber);
|
|
if (existing) {
|
|
seasonsForShow.set(h.seasonNumber, {
|
|
seasonNumber: h.seasonNumber,
|
|
partner: existing.partner ?? h.partner ?? null,
|
|
participants: existing.participants.length
|
|
? existing.participants
|
|
: (h.seasonParticipants ?? []),
|
|
startDate: existing.startDate ?? h.startDate ?? null,
|
|
});
|
|
} else {
|
|
seasonsForShow.set(h.seasonNumber, {
|
|
seasonNumber: h.seasonNumber,
|
|
partner: h.partner ?? null,
|
|
participants: h.seasonParticipants ?? [],
|
|
startDate: h.startDate ?? null,
|
|
});
|
|
}
|
|
|
|
grouped.set(h.showId, seasonsForShow);
|
|
}
|
|
|
|
const showIds = Array.from(grouped.keys());
|
|
|
|
const fromContext = showIds
|
|
.map((id) => shows.find((s) => s.id === id))
|
|
.filter((s): s is (typeof shows)[number] => !!s);
|
|
|
|
const missingIds = showIds.filter(
|
|
(id) => !fromContext.some((s) => s.id === id)
|
|
);
|
|
|
|
const fetched = await Promise.all(
|
|
missingIds.map(async (id) => {
|
|
try {
|
|
const s = await getShowById(id);
|
|
return s;
|
|
} catch {
|
|
return null;
|
|
}
|
|
})
|
|
);
|
|
|
|
const allShows = [
|
|
...fromContext,
|
|
...fetched.filter(Boolean),
|
|
] as typeof shows;
|
|
|
|
const result: AppearanceGroup[] = allShows.map((s) => {
|
|
const seasonsMap = grouped.get(s.id)!;
|
|
const seasonsSorted = Array.from(seasonsMap.values()).sort(
|
|
(a, b) => a.seasonNumber - b.seasonNumber
|
|
);
|
|
return {
|
|
show: {
|
|
id: s.id,
|
|
title: s.title,
|
|
bannerUri: s.bannerUri,
|
|
thumbnailUri: s.thumbnailUri,
|
|
},
|
|
seasons: seasonsSorted,
|
|
};
|
|
});
|
|
|
|
result.sort((a, b) =>
|
|
a.show.title.localeCompare(b.show.title, "de", {
|
|
sensitivity: "base",
|
|
})
|
|
);
|
|
|
|
setAppearances(result);
|
|
} catch (e: any) {
|
|
if (!controller.signal.aborted)
|
|
setError(e?.message || "Fehler beim Laden");
|
|
} finally {
|
|
if (!controller.signal.aborted) setLoading(false);
|
|
}
|
|
})();
|
|
|
|
return () => controller.abort();
|
|
}, [pid, shows]);
|
|
|
|
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 },
|
|
});
|
|
},
|
|
[pid]
|
|
);
|
|
|
|
return (
|
|
<GestureHandlerRootView style={styles.mainContainer}>
|
|
<ScrollView
|
|
showsVerticalScrollIndicator={false}
|
|
contentContainerStyle={{ paddingBottom: 20, paddingTop: 10 }}
|
|
>
|
|
<Text style={styles.participantName}>{name}</Text>
|
|
<TouchableOpacity
|
|
style={styles.closeIcon}
|
|
onPress={() => router.back()}
|
|
>
|
|
<Ionicons name="close-circle-outline" size={38} color="white" />
|
|
</TouchableOpacity>
|
|
|
|
<View style={styles.performedShowsSection}>
|
|
<Text style={styles.performedShowsTitle}>Auftritte:</Text>
|
|
|
|
<ScrollView
|
|
horizontal
|
|
showsHorizontalScrollIndicator={false}
|
|
style={{ flex: 1 }}
|
|
contentContainerStyle={{
|
|
gap: 20,
|
|
paddingHorizontal: 15,
|
|
paddingLeft: 30,
|
|
}}
|
|
>
|
|
{appearances.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>
|
|
<Text style={styles.showTitle} numberOfLines={1}>
|
|
{show.title}
|
|
</Text>
|
|
<Text style={styles.showSeason}>
|
|
({formatYear(seasons[0]?.startDate)})
|
|
</Text>
|
|
<Text style={styles.showSeason}>
|
|
Staffel {seasons.map((s) => s.seasonNumber).join(" und ")}
|
|
</Text>
|
|
|
|
<View style={styles.horizontalLine} />
|
|
|
|
<Text style={[styles.participantLabel, { marginTop: 10 }]}>
|
|
Weitere Teilnehmer
|
|
</Text>
|
|
|
|
<View style={styles.participantContainer}>
|
|
<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>
|
|
|
|
{partners.length > 0 && (
|
|
<>
|
|
<View style={styles.horizontalLine} />
|
|
<Text
|
|
style={[styles.participantLabel, { marginTop: 10 }]}
|
|
>
|
|
Partner
|
|
</Text>
|
|
<View
|
|
style={[
|
|
styles.showContainer,
|
|
{
|
|
backgroundColor: "hsl(221, 39%, 12%)",
|
|
width: 150,
|
|
marginTop: 20,
|
|
},
|
|
]}
|
|
>
|
|
<Image
|
|
style={styles.showImage}
|
|
blurRadius={20}
|
|
source={{
|
|
uri: `https://i.pravatar.cc/300?img=${Math.floor(Math.random() * 70)}`,
|
|
}}
|
|
/>
|
|
</View>
|
|
{partners.map((p) => (
|
|
<Text
|
|
key={p.personId}
|
|
style={styles.partnerLabel}
|
|
numberOfLines={1}
|
|
>
|
|
{p.name}
|
|
</Text>
|
|
))}
|
|
</>
|
|
)}
|
|
</View>
|
|
);
|
|
})}
|
|
</ScrollView>
|
|
</View>
|
|
</ScrollView>
|
|
</GestureHandlerRootView>
|
|
);
|
|
}
|