CoompanionApp/mobile/src/screens/LobbyScreen.tsx

435 lines
13 KiB
TypeScript
Raw Normal View History

2026-02-03 13:48:56 +01:00
import React, { useEffect, useMemo, useState } from "react";
import {
Platform,
2026-03-30 10:38:01 +02:00
ScrollView,
2026-02-03 13:48:56 +01:00
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useNavigation } from "@react-navigation/native";
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
2026-03-30 10:38:01 +02:00
import BrandLockup from "../components/BrandLockup";
import ExitGameButton from "../components/ExitGameButton";
import { useI18n } from "../i18n";
2026-02-03 13:48:56 +01:00
import type { RootStackParamList } from "../navigation/types";
import { useSession } from "../state/session-context";
2026-03-30 10:38:01 +02:00
import { useTheme, type AppTheme } from "../theme";
2026-02-03 13:48:56 +01:00
export default function LobbyScreen() {
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const manager = useSession();
const { t } = useI18n();
const theme = useTheme();
const styles = useMemo(() => createStyles(theme), [theme]);
const placeholderColor = theme.colors.placeholder;
const [dummyName, setDummyName] = useState("");
const [dummyBalance, setDummyBalance] = useState("1500");
const insets = useSafeAreaInsets();
const topInset = insets.top || (Platform.OS === "ios" ? 44 : 0);
2026-03-30 10:38:01 +02:00
const customers = manager.session?.players.filter((player) => player.role !== "banker") ?? [];
const assistedCount = customers.filter((player) => player.isDummy).length;
2026-02-03 13:48:56 +01:00
useEffect(() => {
if (!manager.session || !manager.me) return;
if (manager.session.status === "active") {
navigation.replace(manager.isBanker ? "BankerTabs" : "PlayerTabs");
}
}, [manager.session, manager.me, manager.isBanker, navigation]);
if (!manager.session || !manager.me) {
return (
2026-03-30 10:38:01 +02:00
<View
style={[
styles.loadingContainer,
{
paddingTop: topInset + 20,
paddingBottom: insets.bottom + 20,
paddingLeft: insets.left + 20,
paddingRight: insets.right + 20,
},
]}
>
<BrandLockup variant="hero" subtitle={t("lobby.title")} />
<Text style={styles.loadingText}>{t("common.loadingLobby")}</Text>
2026-02-03 13:48:56 +01:00
{manager.error ? <Text style={styles.helper}>{manager.error}</Text> : null}
<ExitGameButton mode="full" />
</View>
);
}
const session = manager.session;
const me = manager.me;
const canStart = manager.isBanker && session.status === "lobby";
const pendingTakeover = session.takeoverRequests.find(
2026-03-30 10:38:01 +02:00
(request) => request.requesterId === manager.playerId && request.status === "pending",
2026-02-03 16:35:01 +01:00
);
const pendingRequests = manager.isBanker
? session.takeoverRequests.filter((request) => request.status === "pending")
2026-02-03 16:35:01 +01:00
: [];
2026-02-03 13:48:56 +01:00
return (
2026-03-30 10:38:01 +02:00
<ScrollView
style={styles.scroll}
contentContainerStyle={{
paddingTop: topInset + 16,
paddingBottom: insets.bottom + 24,
paddingLeft: insets.left + 20,
paddingRight: insets.right + 20,
gap: 16,
}}
>
<View style={styles.hero}>
<BrandLockup variant="hero" subtitle={t("lobby.title")} onDark />
<Text style={styles.heroTitle}>{t("lobby.code", { code: session.code })}</Text>
<Text style={styles.heroBody}>
{manager.isBanker ? t("lobby.heroAdvisor") : t("lobby.heroCustomer")}
</Text>
<View style={styles.metrics}>
<View style={styles.metricCard}>
<Text style={styles.metricValue}>{customers.length}</Text>
<Text style={styles.metricLabel}>{t("lobby.customers")}</Text>
</View>
<View style={styles.metricCard}>
<Text style={styles.metricValue}>{assistedCount}</Text>
<Text style={styles.metricLabel}>{t("lobby.assisted")}</Text>
</View>
</View>
</View>
2026-02-03 16:35:01 +01:00
{pendingTakeover ? (
2026-03-30 10:38:01 +02:00
<View style={styles.noticeCard}>
<Text style={styles.noticeText}>{t("entry.takeoverPending")}</Text>
</View>
2026-02-03 16:35:01 +01:00
) : null}
2026-02-03 13:48:56 +01:00
2026-03-30 10:38:01 +02:00
<View style={styles.card}>
<Text style={styles.cardTitle}>{t("lobby.rosterTitle")}</Text>
<View style={styles.roster}>
{session.players.map((item) => (
<View key={item.id} style={styles.listItem}>
<View style={styles.listCopy}>
<Text style={styles.playerName}>{item.name}</Text>
<Text style={styles.playerMeta}>
{item.role === "banker" ? t("common.banker") : t("common.player")}
{item.isDummy ? ` · ${t("common.dummy")}` : ""}
</Text>
</View>
<Text style={styles.statusText}>
{item.connected ? t("common.online") : t("common.offline")}
2026-02-03 13:48:56 +01:00
</Text>
</View>
2026-03-30 10:38:01 +02:00
))}
</View>
</View>
2026-02-03 13:48:56 +01:00
2026-02-03 16:35:01 +01:00
{manager.isBanker && pendingRequests.length > 0 ? (
<View style={styles.card}>
<Text style={styles.cardTitle}>{t("banker.takeoverApprovals")}</Text>
<View style={styles.takeoverList}>
{pendingRequests.map((request) => {
const requester =
session.players.find((player) => player.id === request.requesterId) ?? null;
2026-02-03 16:35:01 +01:00
const dummy =
session.players.find((player) => player.id === request.dummyId) ?? null;
2026-02-03 16:35:01 +01:00
const requesterName =
requester?.name ?? request.requesterName ?? t("common.player");
return (
<View key={request.id} style={styles.takeoverRow}>
<View style={styles.takeoverMeta}>
<Text style={styles.takeoverName}>{requesterName}</Text>
<Text style={styles.takeoverSub}>
{t("banker.wants", { name: dummy?.name ?? t("common.dummy") })}
</Text>
</View>
<TouchableOpacity
style={styles.buttonSmall}
onPress={() =>
manager.sendMessage({
2026-03-30 10:38:01 +02:00
type: "banker_takeover_approve",
sessionId: manager.sessionId,
bankerId: me.id,
dummyId: request.dummyId,
requesterId: request.requesterId,
})
2026-02-03 16:35:01 +01:00
}
>
<Text style={styles.buttonSmallText}>{t("banker.approve")}</Text>
</TouchableOpacity>
</View>
);
})}
</View>
</View>
) : null}
2026-03-30 10:38:01 +02:00
{manager.isBanker ? (
2026-02-03 13:48:56 +01:00
<View style={styles.card}>
<Text style={styles.cardTitle}>{t("lobby.addDummyTitle")}</Text>
2026-03-30 10:38:01 +02:00
<Text style={styles.cardSubtitle}>{t("lobby.addDummySubtitle")}</Text>
2026-02-03 13:48:56 +01:00
<TextInput
style={styles.input}
placeholder={t("lobby.enterDummyName")}
placeholderTextColor={placeholderColor}
value={dummyName}
onChangeText={setDummyName}
/>
<TextInput
style={styles.input}
placeholder={t("banker.tools.startingBalance")}
placeholderTextColor={placeholderColor}
value={dummyBalance}
onChangeText={setDummyBalance}
keyboardType="number-pad"
/>
<TouchableOpacity
style={styles.buttonSecondary}
onPress={() => {
manager.sendMessage({
type: "banker_create_dummy",
sessionId: manager.sessionId,
bankerId: me.id,
2026-02-03 13:48:56 +01:00
name: dummyName,
balance: Number(dummyBalance) || undefined,
});
setDummyName("");
setDummyBalance("1500");
}}
>
<Text style={styles.buttonSecondaryText}>{t("lobby.addDummyButton")}</Text>
</TouchableOpacity>
</View>
2026-03-30 10:38:01 +02:00
) : (
<View style={styles.waitingCard}>
<Text style={styles.waitingTitle}>{t("lobby.waitingTitle")}</Text>
<Text style={styles.waitingBody}>{t("lobby.waitingBody")}</Text>
</View>
2026-02-03 13:48:56 +01:00
)}
2026-03-30 10:38:01 +02:00
{canStart ? (
2026-02-03 13:48:56 +01:00
<TouchableOpacity
style={styles.button}
onPress={() =>
manager.sendMessage({
type: "banker_start",
sessionId: manager.sessionId,
bankerId: me.id,
2026-02-03 13:48:56 +01:00
})
}
>
<Text style={styles.buttonText}>{t("lobby.startGame")}</Text>
</TouchableOpacity>
2026-03-30 10:38:01 +02:00
) : null}
2026-02-03 13:48:56 +01:00
<ExitGameButton mode="full" />
2026-03-30 10:38:01 +02:00
</ScrollView>
2026-02-03 13:48:56 +01:00
);
}
const createStyles = (theme: AppTheme) =>
StyleSheet.create({
2026-03-30 10:38:01 +02:00
scroll: {
2026-02-03 13:48:56 +01:00
flex: 1,
backgroundColor: theme.colors.background,
},
2026-03-30 10:38:01 +02:00
loadingContainer: {
flex: 1,
gap: 16,
backgroundColor: theme.colors.background,
justifyContent: "center",
},
loadingText: {
color: theme.colors.text,
fontSize: 20,
2026-02-03 13:48:56 +01:00
fontWeight: "700",
2026-03-30 10:38:01 +02:00
},
hero: {
borderRadius: 28,
padding: 22,
gap: 12,
backgroundColor: theme.colors.brandSurface,
borderWidth: 1,
borderColor: theme.colors.brandSurfaceAlt,
},
heroTitle: {
color: theme.colors.brandText,
fontSize: 26,
fontWeight: "800",
letterSpacing: -0.8,
},
heroBody: {
color: theme.colors.brandTextMuted,
fontSize: 15,
lineHeight: 22,
},
metrics: {
flexDirection: "row",
gap: 12,
},
metricCard: {
flex: 1,
borderRadius: 18,
padding: 14,
backgroundColor: theme.colors.brandSurfaceAlt,
gap: 4,
},
metricValue: {
color: theme.colors.brandText,
fontSize: 22,
fontWeight: "800",
},
metricLabel: {
color: theme.colors.brandTextMuted,
fontSize: 12,
textTransform: "uppercase",
letterSpacing: 1,
},
noticeCard: {
borderRadius: 18,
padding: 14,
backgroundColor: theme.colors.warningSurface,
borderWidth: 1,
borderColor: theme.colors.warningBorder,
},
noticeText: {
color: theme.colors.warningTextStrong,
fontWeight: "600",
},
card: {
backgroundColor: theme.colors.surface,
borderRadius: 24,
padding: 20,
gap: 12,
borderWidth: 1,
borderColor: theme.colors.border,
},
cardTitle: {
2026-02-03 13:48:56 +01:00
color: theme.colors.text,
2026-03-30 10:38:01 +02:00
fontSize: 20,
fontWeight: "700",
2026-02-03 13:48:56 +01:00
},
2026-03-30 10:38:01 +02:00
cardSubtitle: {
2026-02-03 13:48:56 +01:00
color: theme.colors.textMuted,
2026-03-30 10:38:01 +02:00
lineHeight: 21,
2026-02-03 13:48:56 +01:00
},
2026-03-30 10:38:01 +02:00
roster: {
2026-02-03 13:48:56 +01:00
gap: 10,
},
listItem: {
2026-03-30 10:38:01 +02:00
backgroundColor: theme.colors.surfaceAlt,
borderRadius: 18,
padding: 14,
2026-02-03 13:48:56 +01:00
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
2026-03-30 10:38:01 +02:00
gap: 12,
},
listCopy: {
flex: 1,
gap: 3,
2026-02-03 13:48:56 +01:00
},
playerName: {
2026-03-30 10:38:01 +02:00
fontWeight: "700",
2026-02-03 13:48:56 +01:00
color: theme.colors.text,
},
playerMeta: {
fontSize: 12,
color: theme.colors.textMuted,
},
2026-03-30 10:38:01 +02:00
statusText: {
2026-02-03 13:48:56 +01:00
color: theme.colors.textMuted,
fontSize: 12,
2026-03-30 10:38:01 +02:00
fontWeight: "600",
textTransform: "uppercase",
letterSpacing: 0.8,
2026-02-03 13:48:56 +01:00
},
input: {
borderWidth: 1,
borderColor: theme.colors.border,
backgroundColor: theme.colors.inputBackground,
color: theme.colors.inputText,
2026-03-30 10:38:01 +02:00
borderRadius: 16,
paddingHorizontal: 14,
paddingVertical: 13,
2026-02-03 13:48:56 +01:00
},
button: {
backgroundColor: theme.colors.primary,
2026-03-30 10:38:01 +02:00
paddingVertical: 15,
2026-02-03 13:48:56 +01:00
borderRadius: 999,
alignItems: "center",
},
buttonText: {
color: theme.colors.primaryText,
2026-03-30 10:38:01 +02:00
fontWeight: "700",
2026-02-03 13:48:56 +01:00
},
buttonSecondary: {
backgroundColor: theme.colors.secondary,
2026-03-30 10:38:01 +02:00
paddingVertical: 14,
2026-02-03 13:48:56 +01:00
borderRadius: 999,
alignItems: "center",
},
buttonSecondaryText: {
color: theme.colors.secondaryText,
2026-03-30 10:38:01 +02:00
fontWeight: "700",
},
waitingCard: {
backgroundColor: theme.colors.accentSurface,
borderRadius: 24,
padding: 20,
gap: 8,
borderWidth: 1,
borderColor: theme.colors.borderMuted,
},
waitingTitle: {
color: theme.colors.text,
fontSize: 18,
fontWeight: "700",
},
waitingBody: {
color: theme.colors.textMuted,
lineHeight: 21,
},
helper: {
color: theme.colors.textMuted,
fontSize: 12,
2026-02-03 13:48:56 +01:00
},
2026-02-03 16:35:01 +01:00
takeoverList: {
gap: 10,
},
takeoverRow: {
2026-03-30 10:38:01 +02:00
borderRadius: 18,
padding: 14,
backgroundColor: theme.colors.surfaceAlt,
borderWidth: 1,
borderColor: theme.colors.borderMuted,
2026-02-03 16:35:01 +01:00
flexDirection: "row",
alignItems: "center",
2026-03-30 10:38:01 +02:00
gap: 12,
2026-02-03 16:35:01 +01:00
},
takeoverMeta: {
flex: 1,
2026-03-30 10:38:01 +02:00
gap: 3,
2026-02-03 16:35:01 +01:00
},
takeoverName: {
2026-03-30 10:38:01 +02:00
fontWeight: "700",
2026-02-03 16:35:01 +01:00
color: theme.colors.text,
},
takeoverSub: {
color: theme.colors.textMuted,
2026-03-30 10:38:01 +02:00
fontSize: 12,
2026-02-03 16:35:01 +01:00
},
buttonSmall: {
2026-03-30 10:38:01 +02:00
backgroundColor: theme.colors.primary,
2026-02-03 16:35:01 +01:00
borderRadius: 999,
2026-03-30 10:38:01 +02:00
paddingHorizontal: 14,
paddingVertical: 10,
2026-02-03 16:35:01 +01:00
},
buttonSmallText: {
2026-03-30 10:38:01 +02:00
color: theme.colors.primaryText,
fontWeight: "700",
2026-02-03 16:35:01 +01:00
},
2026-02-03 13:48:56 +01:00
});