updated
This commit is contained in:
@@ -11,6 +11,12 @@ export type PersonHistoryEntry = {
|
|||||||
showId: number;
|
showId: number;
|
||||||
seasonId: number;
|
seasonId: number;
|
||||||
seasonNumber: number;
|
seasonNumber: number;
|
||||||
|
partner?: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
birthDate?: string;
|
||||||
|
imageUri: string | null;
|
||||||
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PERSON_API_BASE = "http://45.157.177.99:8080/persons";
|
const PERSON_API_BASE = "http://45.157.177.99:8080/persons";
|
||||||
@@ -28,11 +34,25 @@ export async function getPersonHistory(
|
|||||||
console.warn("History expected array, got:", data);
|
console.warn("History expected array, got:", data);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return (data as PersonHistorySeasonRaw[]).map((s) => ({
|
return (data as PersonHistorySeasonRaw[]).map((season) => {
|
||||||
showId: s.show,
|
const me = season.seasonParticipants.find(
|
||||||
seasonId: s.seasonId,
|
(p) => p.person.personId === personId
|
||||||
seasonNumber: s.seasonNumber,
|
);
|
||||||
}));
|
let partner: PersonHistoryEntry["partner"] = null;
|
||||||
|
if (me?.partner?.person) {
|
||||||
|
partner = {
|
||||||
|
id: me.partner.person.personId,
|
||||||
|
name: me.partner.person.name,
|
||||||
|
imageUri: me.partner.person.imageUrl ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
showId: season.show,
|
||||||
|
seasonId: season.seasonId,
|
||||||
|
seasonNumber: season.seasonNumber,
|
||||||
|
partner,
|
||||||
|
};
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("getPersonHistory error:", e);
|
console.error("getPersonHistory error:", e);
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export async function getSeason(
|
|||||||
: undefined,
|
: undefined,
|
||||||
imageUri:
|
imageUri:
|
||||||
p.person.imageUrl ??
|
p.person.imageUrl ??
|
||||||
"https://via.placeholder.com/300x400.png?text=No+Image",
|
`https://i.pravatar.cc/300?img=${Math.random() * 70}`,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ export async function getShows(): Promise<Show[]> {
|
|||||||
concept: s.concept,
|
concept: s.concept,
|
||||||
running: s.running,
|
running: s.running,
|
||||||
logoUri: s.logoUrl ?? "",
|
logoUri: s.logoUrl ?? "",
|
||||||
|
startDate: s.startDate,
|
||||||
|
endDate: s.endDate,
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Fetch error:", error);
|
console.error("Fetch error:", error);
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export default function HomeScreen() {
|
|||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={serviceName}
|
key={serviceName}
|
||||||
style={{
|
style={{
|
||||||
padding: 3,
|
padding: 2,
|
||||||
backgroundColor: "hsl(221, 39%, 80%)",
|
backgroundColor: "hsl(221, 39%, 80%)",
|
||||||
borderRadius: 50,
|
borderRadius: 50,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import Ionicons from "@expo/vector-icons/Ionicons";
|
|||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import { router, useLocalSearchParams } from "expo-router";
|
import { router, useLocalSearchParams } from "expo-router";
|
||||||
import { usePersonContext } from "@/contexts/PersonContext";
|
import { usePersonContext } from "@/contexts/PersonContext";
|
||||||
|
import * as WebBrowser from "expo-web-browser";
|
||||||
import {
|
import {
|
||||||
ScrollView,
|
ScrollView,
|
||||||
GestureHandlerRootView,
|
GestureHandlerRootView,
|
||||||
} from "react-native-gesture-handler";
|
} from "react-native-gesture-handler";
|
||||||
import { useShowContext } from "@/contexts/ShowContext";
|
import { useShowContext } from "@/contexts/ShowContext";
|
||||||
import { getPersonHistory } from "@/apis/personApi";
|
|
||||||
|
|
||||||
export default function ParticipantScreen() {
|
export default function ParticipantScreen() {
|
||||||
const { getPersonAppearances, isLoading, getError } = usePersonContext();
|
const { getPersonAppearances, isLoading, getError } = usePersonContext();
|
||||||
@@ -34,8 +34,9 @@ export default function ParticipantScreen() {
|
|||||||
const grouped = data.showIds.map((id) => ({
|
const grouped = data.showIds.map((id) => ({
|
||||||
showId: id,
|
showId: id,
|
||||||
seasons: data.byShow[id],
|
seasons: data.byShow[id],
|
||||||
|
partners: data.partnersByShow[id] || [],
|
||||||
}));
|
}));
|
||||||
setAppearances(grouped);
|
setAppearances(grouped as any);
|
||||||
})();
|
})();
|
||||||
return () => {
|
return () => {
|
||||||
active = false;
|
active = false;
|
||||||
@@ -44,14 +45,30 @@ export default function ParticipantScreen() {
|
|||||||
|
|
||||||
const resolved = useMemo(
|
const resolved = useMemo(
|
||||||
() =>
|
() =>
|
||||||
appearances
|
(appearances as any[])
|
||||||
.map((a) => {
|
.map((a) => {
|
||||||
const show = shows.find((s) => s.id === a.showId);
|
const show = shows.find((s) => s.id === a.showId);
|
||||||
if (!show) return null;
|
if (!show) return null;
|
||||||
return { show, seasons: a.seasons };
|
return {
|
||||||
|
show,
|
||||||
|
seasons: a.seasons as number[],
|
||||||
|
partners: a.partners as {
|
||||||
|
seasonNumber: number;
|
||||||
|
partner?: { id: number; name: string; imageUrl?: string | null };
|
||||||
|
}[],
|
||||||
|
};
|
||||||
})
|
})
|
||||||
.filter(
|
.filter(
|
||||||
(v): v is { show: (typeof shows)[number]; seasons: number[] } => !!v
|
(
|
||||||
|
v
|
||||||
|
): v is {
|
||||||
|
show: (typeof shows)[number];
|
||||||
|
seasons: number[];
|
||||||
|
partners: {
|
||||||
|
seasonNumber: number;
|
||||||
|
partner?: { id: number; name: string; imageUrl?: string | null };
|
||||||
|
}[];
|
||||||
|
} => !!v
|
||||||
),
|
),
|
||||||
[appearances, shows]
|
[appearances, shows]
|
||||||
);
|
);
|
||||||
@@ -75,12 +92,6 @@ export default function ParticipantScreen() {
|
|||||||
<View style={styles.dot} />
|
<View style={styles.dot} />
|
||||||
<Text style={styles.participantInfo}>Köln</Text>
|
<Text style={styles.participantInfo}>Köln</Text>
|
||||||
</View>
|
</View>
|
||||||
<Image
|
|
||||||
source={{
|
|
||||||
uri: "https://www.fernseh-puls.com/wp-content/uploads/are-you-the-one-calvin-o-im-steckbrief-wir-stellen-euch-den-kandidaten-vor.jpg",
|
|
||||||
}}
|
|
||||||
style={styles.participantImage}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<View style={styles.performedShowsSection}>
|
<View style={styles.performedShowsSection}>
|
||||||
<Text style={styles.performedShowsTitle}>Auftritte:</Text>
|
<Text style={styles.performedShowsTitle}>Auftritte:</Text>
|
||||||
@@ -107,23 +118,31 @@ export default function ParticipantScreen() {
|
|||||||
marginTop: 15,
|
marginTop: 15,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{resolved.map(({ show, seasons }) => (
|
{resolved.map(({ show, seasons, partners }) => {
|
||||||
<TouchableOpacity key={show.id} style={styles.showContainer}>
|
const seasonPartnerLines = partners.map((p) => {
|
||||||
<Image
|
const label = `Staffel ${p.seasonNumber}`;
|
||||||
source={{ uri: show.thumbnailUri }}
|
if (!p.partner) return label;
|
||||||
style={styles.showImage}
|
return `${label} • Partner: ${p.partner.name}`;
|
||||||
/>
|
});
|
||||||
<Text style={styles.showTitle} numberOfLines={2}>
|
return (
|
||||||
{show.title}
|
<TouchableOpacity key={show.id} style={styles.showContainer}>
|
||||||
</Text>
|
<Image
|
||||||
<Text style={styles.showSeason} numberOfLines={1}>
|
source={{ uri: show.thumbnailUri }}
|
||||||
Staffel
|
style={styles.showImage}
|
||||||
{seasons.length === 1
|
/>
|
||||||
? ` ${seasons[0]}`
|
<Text style={styles.showTitle} numberOfLines={2}>
|
||||||
: `n ${seasons.join(", ")}`}
|
{show.title}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
|
||||||
))}
|
<Text
|
||||||
|
style={styles.showSeason}
|
||||||
|
numberOfLines={seasonPartnerLines.length}
|
||||||
|
>
|
||||||
|
{seasonPartnerLines.join("\n")}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import ShowInfo from "@/components/ui/ShowInfo";
|
|||||||
import ParticipantDetails from "@/components/ParticipantDeatails";
|
import ParticipantDetails from "@/components/ParticipantDeatails";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useSeasonContext } from "@/contexts/SeasonContext";
|
import { useSeasonContext } from "@/contexts/SeasonContext";
|
||||||
|
import { getShowColors } from "@/constants/colors";
|
||||||
import {
|
import {
|
||||||
Dimensions,
|
Dimensions,
|
||||||
Image,
|
Image,
|
||||||
@@ -12,7 +13,6 @@ import {
|
|||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import * as WebBrowser from "expo-web-browser";
|
|
||||||
import styles from "./stackStyles/showDetailStyles";
|
import styles from "./stackStyles/showDetailStyles";
|
||||||
|
|
||||||
export default function ShowDetails() {
|
export default function ShowDetails() {
|
||||||
@@ -23,6 +23,8 @@ export default function ShowDetails() {
|
|||||||
genres,
|
genres,
|
||||||
streamingService,
|
streamingService,
|
||||||
id,
|
id,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
logoUri,
|
logoUri,
|
||||||
} = useLocalSearchParams();
|
} = useLocalSearchParams();
|
||||||
const [selectedParticipants, setSelectedParticipants] =
|
const [selectedParticipants, setSelectedParticipants] =
|
||||||
@@ -37,6 +39,8 @@ export default function ShowDetails() {
|
|||||||
const [pLoading, setPLoading] = React.useState(false);
|
const [pLoading, setPLoading] = React.useState(false);
|
||||||
const [pError, setPError] = React.useState<string | null>(null);
|
const [pError, setPError] = React.useState<string | null>(null);
|
||||||
|
|
||||||
|
const { tabColor, seasonColor } = getShowColors(Number(id));
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!showId) return;
|
if (!showId) return;
|
||||||
let active = true;
|
let active = true;
|
||||||
@@ -85,6 +89,8 @@ export default function ShowDetails() {
|
|||||||
seasons={seasonCount}
|
seasons={seasonCount}
|
||||||
participants={participants.length}
|
participants={participants.length}
|
||||||
streamingService={streamingService as string}
|
streamingService={streamingService as string}
|
||||||
|
startDate={startDate as string}
|
||||||
|
endDate={endDate as string | null}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<View style={styles.showBannerLogoContainer}>
|
<View style={styles.showBannerLogoContainer}>
|
||||||
@@ -103,7 +109,7 @@ export default function ShowDetails() {
|
|||||||
styles.infoLabel,
|
styles.infoLabel,
|
||||||
{
|
{
|
||||||
fontWeight: selectedParticipants ? "bold" : "normal",
|
fontWeight: selectedParticipants ? "bold" : "normal",
|
||||||
color: selectedParticipants ? "#199edb" : "hsl(0, 0%, 65%)",
|
color: selectedParticipants ? tabColor : "hsl(0, 0%, 65%)",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -142,7 +148,7 @@ export default function ShowDetails() {
|
|||||||
{
|
{
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
selectedSeason === season
|
selectedSeason === season
|
||||||
? "#199edb"
|
? seasonColor
|
||||||
: "hsl(0, 0%, 20%)",
|
: "hsl(0, 0%, 20%)",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
@@ -172,7 +178,10 @@ export default function ShowDetails() {
|
|||||||
{participants.map((p) => (
|
{participants.map((p) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={p.id}
|
key={p.id}
|
||||||
style={styles.participantContainer}
|
style={[
|
||||||
|
styles.participantContainer,
|
||||||
|
{ backgroundColor: tabColor },
|
||||||
|
]}
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
router.push({
|
router.push({
|
||||||
pathname: "/participant",
|
pathname: "/participant",
|
||||||
@@ -190,6 +199,8 @@ export default function ShowDetails() {
|
|||||||
height: "100%",
|
height: "100%",
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
}}
|
}}
|
||||||
|
resizeMode="cover"
|
||||||
|
blurRadius={p.imageUri.includes("pravatar") ? 16 : 0}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.participantLabel} numberOfLines={2}>
|
<Text style={styles.participantLabel} numberOfLines={2}>
|
||||||
{p.name}
|
{p.name}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
performedShowsSection: {
|
performedShowsSection: {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: 375,
|
height: "100%",
|
||||||
paddingLeft: 15,
|
paddingLeft: 15,
|
||||||
paddingBottom: 20,
|
paddingBottom: 20,
|
||||||
backgroundColor: "hsl(221, 39%, 0%)",
|
backgroundColor: "hsl(221, 39%, 0%)",
|
||||||
|
|||||||
@@ -4,12 +4,16 @@ type ShowInfoProps = {
|
|||||||
seasons: number;
|
seasons: number;
|
||||||
participants: number;
|
participants: number;
|
||||||
streamingService: string;
|
streamingService: string;
|
||||||
|
startDate: string;
|
||||||
|
endDate?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ShowInfo = ({
|
const ShowInfo = ({
|
||||||
seasons,
|
seasons,
|
||||||
participants,
|
participants,
|
||||||
streamingService,
|
streamingService,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
}: ShowInfoProps) => {
|
}: ShowInfoProps) => {
|
||||||
return (
|
return (
|
||||||
<View style={styles.showMainInfoSection}>
|
<View style={styles.showMainInfoSection}>
|
||||||
|
|||||||
20
constants/colors.ts
Normal file
20
constants/colors.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export type ShowDetailColors = {
|
||||||
|
tabColor: string;
|
||||||
|
seasonColor: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ShowColors: Record<number, ShowDetailColors> = {
|
||||||
|
1: { tabColor: "#009fe2", seasonColor: "#ec5691" },
|
||||||
|
2: { tabColor: "#ffffff", seasonColor: "#3fa8f3" },
|
||||||
|
3: { tabColor: "#ffffff", seasonColor: "#000000" },
|
||||||
|
4: { tabColor: "#8fb4f3", seasonColor: "#4b046c" },
|
||||||
|
5: { tabColor: "#ffffff", seasonColor: "#000000" },
|
||||||
|
6: { tabColor: "#ffffff", seasonColor: "#ff0000" },
|
||||||
|
7: { tabColor: "#25322c", seasonColor: "#957a45" },
|
||||||
|
8: { tabColor: "#ffffff", seasonColor: "#0179b5" },
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getShowColors(showId: number): ShowDetailColors {
|
||||||
|
if (!showId) return { tabColor: "#009fe2", seasonColor: "#ec5691" };
|
||||||
|
return ShowColors[showId] || { tabColor: "#009fe2", seasonColor: "#ec5691" };
|
||||||
|
}
|
||||||
@@ -11,6 +11,10 @@ type PersonAppearances = {
|
|||||||
raw: PersonHistoryEntry[];
|
raw: PersonHistoryEntry[];
|
||||||
byShow: Record<number, number[]>;
|
byShow: Record<number, number[]>;
|
||||||
showIds: number[];
|
showIds: number[];
|
||||||
|
partnersByShow: Record<
|
||||||
|
number,
|
||||||
|
{ seasonNumber: number; partner?: PersonHistoryEntry["partner"] }[]
|
||||||
|
>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PersonContextType = {
|
type PersonContextType = {
|
||||||
@@ -32,23 +36,34 @@ export const PersonProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
const buildAppearances = (
|
const buildAppearances = (
|
||||||
entries: PersonHistoryEntry[]
|
entries: PersonHistoryEntry[]
|
||||||
): PersonAppearances => {
|
): PersonAppearances => {
|
||||||
const byShow: Record<number, Set<number>> = {};
|
const byShowSet: Record<number, Set<number>> = {};
|
||||||
|
const partnersByShow: PersonAppearances["partnersByShow"] = {};
|
||||||
for (const e of entries) {
|
for (const e of entries) {
|
||||||
if (!byShow[e.showId]) byShow[e.showId] = new Set();
|
if (!byShowSet[e.showId]) byShowSet[e.showId] = new Set();
|
||||||
byShow[e.showId].add(e.seasonNumber);
|
byShowSet[e.showId].add(e.seasonNumber);
|
||||||
|
if (!partnersByShow[e.showId]) partnersByShow[e.showId] = [];
|
||||||
|
partnersByShow[e.showId].push({
|
||||||
|
seasonNumber: e.seasonNumber,
|
||||||
|
partner: e.partner ?? undefined,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const byShowSorted: Record<number, number[]> = Object.fromEntries(
|
const byShow: Record<number, number[]> = Object.fromEntries(
|
||||||
Object.entries(byShow).map(([showId, seasonsSet]) => [
|
Object.entries(byShowSet).map(([showId, seasons]) => [
|
||||||
Number(showId),
|
Number(showId),
|
||||||
Array.from(seasonsSet).sort((a, b) => a - b),
|
Array.from(seasons).sort((a, b) => a - b),
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Object.values(partnersByShow).forEach((arr) =>
|
||||||
|
arr.sort((a, b) => a.seasonNumber - b.seasonNumber)
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
raw: entries,
|
raw: entries,
|
||||||
byShow: byShowSorted,
|
byShow,
|
||||||
showIds: Object.keys(byShowSorted)
|
showIds: Object.keys(byShow)
|
||||||
.map(Number)
|
.map(Number)
|
||||||
.sort((a, b) => a - b),
|
.sort((a, b) => a - b),
|
||||||
|
partnersByShow,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -65,7 +80,7 @@ export const PersonProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
...err,
|
...err,
|
||||||
[personId]: e?.message || "Fehler beim Laden",
|
[personId]: e?.message || "Fehler beim Laden",
|
||||||
}));
|
}));
|
||||||
return { raw: [], byShow: {}, showIds: [] };
|
return { raw: [], byShow: {}, showIds: [], partnersByShow: {} };
|
||||||
} finally {
|
} finally {
|
||||||
setLoading((l) => ({ ...l, [personId]: false }));
|
setLoading((l) => ({ ...l, [personId]: false }));
|
||||||
}
|
}
|
||||||
@@ -90,7 +105,7 @@ export const PersonProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
const getSeasonsForShow = useCallback(
|
const getSeasonsForShow = useCallback(
|
||||||
async (personId: number, showId: number) => {
|
async (personId: number, showId: number) => {
|
||||||
const app = await getPersonAppearances(personId);
|
const app = await getPersonAppearances(personId);
|
||||||
return app.byShow[showId] || [];
|
return (app.byShow as Record<number, number[]>)[showId] || [];
|
||||||
},
|
},
|
||||||
[getPersonAppearances]
|
[getPersonAppearances]
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user