added games screen

games screen added to bottom navbar. Now user can go to games screen and select private or public game type
This commit is contained in:
suddenlyoranges committed 2026-04-08 21:03:03 +01:00
1 parent 93fb30a7bb
commit 4d5eac38b7
5 files changed
+379 -78

No files matched your search

+64 -28
View File
@@ -1,8 +1,8 @@
import {useEffect, useState} from "react"; import { useEffect, useState } from "react";
import {ActivityIndicator, View} from "react-native"; import { ActivityIndicator, View } from "react-native";
import {NavigationContainer} from "@react-navigation/native"; import { NavigationContainer } from "@react-navigation/native";
import {createNativeStackNavigator} from "@react-navigation/native-stack"; import { createNativeStackNavigator } from "@react-navigation/native-stack";
import {SafeAreaProvider} from "react-native-safe-area-context"; import { SafeAreaProvider } from "react-native-safe-area-context";
import LoginScreen from "./src/components/screens/LoginScreen"; import LoginScreen from "./src/components/screens/LoginScreen";
import RegisterScreen from "./src/components/screens/RegisterScreen"; import RegisterScreen from "./src/components/screens/RegisterScreen";
import TeamScreen from "./src/components/screens/TeamScreen"; import TeamScreen from "./src/components/screens/TeamScreen";
@@ -11,7 +11,8 @@ import LeaderboardScreen from "./src/components/screens/LeaderboardScreen";
import GameSettingsScreen from "./src/components/screens/GameSettingsScreen"; import GameSettingsScreen from "./src/components/screens/GameSettingsScreen";
import PlayersScreen from "./src/components/screens/PlayersScreen"; import PlayersScreen from "./src/components/screens/PlayersScreen";
import ExpandedMapScreen from "./src/components/screens/ExpandedMapScreen"; import ExpandedMapScreen from "./src/components/screens/ExpandedMapScreen";
import {loadSession} from "./src/hooks/SessionStore"; import GameSelectionScreen from "./src/components/screens/GameSelectionScreen";
import { loadSession } from "./src/hooks/SessionStore";
const RootStack = createNativeStackNavigator(); const RootStack = createNativeStackNavigator();
const AuthStackNav = createNativeStackNavigator(); const AuthStackNav = createNativeStackNavigator();
@@ -20,46 +21,71 @@ const AdminStackNav = createNativeStackNavigator();
const screenOptions = { const screenOptions = {
headerShown: false, headerShown: false,
contentStyle: {backgroundColor: '#ffffff'}, contentStyle: { backgroundColor: "#ffffff" },
}; };
const AuthStack = () => ( const AuthStack = () => (
<AuthStackNav.Navigator screenOptions={{...screenOptions, keyboardHandlingEnabled: false}}> <AuthStackNav.Navigator
<AuthStackNav.Screen name="LoginScreen" component={LoginScreen}/> screenOptions={{ ...screenOptions, keyboardHandlingEnabled: false }}
<AuthStackNav.Screen name="RegisterScreen" component={RegisterScreen}/> >
<AuthStackNav.Screen name="LoginScreen" component={LoginScreen} />
<AuthStackNav.Screen name="RegisterScreen" component={RegisterScreen} />
</AuthStackNav.Navigator> </AuthStackNav.Navigator>
); );
const GameStack = () => ( const GameStack = () => (
<GameStackNav.Navigator screenOptions={screenOptions}> <GameStackNav.Navigator screenOptions={screenOptions}>
<GameStackNav.Screen name="MapScreen" component={MapScreen}/> <GameStackNav.Screen
<GameStackNav.Screen name="TeamScreen" component={TeamScreen}/> name="GameSelectionScreen"
<GameStackNav.Screen name="LeaderboardScreen" component={LeaderboardScreen}/> component={GameSelectionScreen}
<GameStackNav.Screen name="PlayersScreen" component={PlayersScreen}/> />
<GameStackNav.Screen name="ExpandedMapScreen" component={ExpandedMapScreen}/> <GameStackNav.Screen name="MapScreen" component={MapScreen} />
<GameStackNav.Screen name="TeamScreen" component={TeamScreen} />
<GameStackNav.Screen
name="LeaderboardScreen"
component={LeaderboardScreen}
/>
<GameStackNav.Screen name="PlayersScreen" component={PlayersScreen} />
<GameStackNav.Screen
name="ExpandedMapScreen"
component={ExpandedMapScreen}
/>
</GameStackNav.Navigator> </GameStackNav.Navigator>
); );
const AdminStack = () => ( const AdminStack = () => (
<AdminStackNav.Navigator screenOptions={screenOptions}> <AdminStackNav.Navigator screenOptions={screenOptions}>
<AdminStackNav.Screen name="GameSettingsScreen" component={GameSettingsScreen}/> <AdminStackNav.Screen
<AdminStackNav.Screen name="MapScreen" component={MapScreen}/> name="GameSelectionScreen"
<AdminStackNav.Screen name="LeaderboardScreen" component={LeaderboardScreen}/> component={GameSelectionScreen}
<AdminStackNav.Screen name="PlayersScreen" component={PlayersScreen}/> />
<AdminStackNav.Screen name="ExpandedMapScreen" component={ExpandedMapScreen}/> <AdminStackNav.Screen
<AdminStackNav.Screen name="TeamScreen" component={TeamScreen}/> name="GameSettingsScreen"
component={GameSettingsScreen}
/>
<AdminStackNav.Screen name="MapScreen" component={MapScreen} />
<AdminStackNav.Screen
name="LeaderboardScreen"
component={LeaderboardScreen}
/>
<AdminStackNav.Screen name="PlayersScreen" component={PlayersScreen} />
<AdminStackNav.Screen
name="ExpandedMapScreen"
component={ExpandedMapScreen}
/>
<AdminStackNav.Screen name="TeamScreen" component={TeamScreen} />
</AdminStackNav.Navigator> </AdminStackNav.Navigator>
); );
export const App = () => { export const App = () => {
// Load persisted session from AsyncStorage before choosing initial route // Load persisted session from AsyncStorage before choosing initial route
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [initialRoute, setInitialRoute] = useState('Auth'); const [initialRoute, setInitialRoute] = useState("Auth");
useEffect(() => { useEffect(() => {
loadSession().then((session) => { loadSession().then((session) => {
if (session.currentUid) { if (session.currentUid) {
setInitialRoute(session.isAcceptedAdmin ? 'Admin' : 'Game'); setInitialRoute(session.isAcceptedAdmin ? "Admin" : "Game");
} }
setLoading(false); setLoading(false);
}); });
@@ -67,7 +93,14 @@ export const App = () => {
if (loading) { if (loading) {
return ( return (
<View style={{flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#fff'}}> <View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
backgroundColor: "#fff",
}}
>
<ActivityIndicator size="large" /> <ActivityIndicator size="large" />
</View> </View>
); );
@@ -76,10 +109,13 @@ export const App = () => {
return ( return (
<SafeAreaProvider> <SafeAreaProvider>
<NavigationContainer> <NavigationContainer>
<RootStack.Navigator initialRouteName={initialRoute} screenOptions={screenOptions}> <RootStack.Navigator
<RootStack.Screen name="Auth" component={AuthStack}/> initialRouteName={initialRoute}
<RootStack.Screen name="Game" component={GameStack}/> screenOptions={screenOptions}
<RootStack.Screen name="Admin" component={AdminStack}/> >
<RootStack.Screen name="Auth" component={AuthStack} />
<RootStack.Screen name="Game" component={GameStack} />
<RootStack.Screen name="Admin" component={AdminStack} />
</RootStack.Navigator> </RootStack.Navigator>
</NavigationContainer> </NavigationContainer>
</SafeAreaProvider> </SafeAreaProvider>
+92 -41
View File
@@ -1,67 +1,118 @@
import {Alert, Pressable, StyleSheet, Text, View} from 'react-native'; import { Alert, Pressable, StyleSheet, Text, View } from "react-native";
import {useSafeAreaInsets} from 'react-native-safe-area-context'; import { useSafeAreaInsets } from "react-native-safe-area-context";
import {clearGameSession, getSession} from '../../hooks/SessionStore'; import { clearGameSession, getSession } from "../../hooks/SessionStore";
import API, {API_BASE_URL} from '../API/API'; import API, { API_BASE_URL } from "../API/API";
const BottomNavbar = ({navigation, routeName}) => { const BottomNavbar = ({ navigation, routeName }) => {
// Initialisation ------------ // Initialisation ------------
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const session = getSession(); const session = getSession();
const adminTabs = [ const adminTabs = [
{label: 'Settings', route: 'GameSettingsScreen', onPress: () => navigation.navigate('GameSettingsScreen')}, {
{label: 'Map', route: 'MapScreen', onPress: () => navigation.navigate('MapScreen')}, label: "Settings",
{label: 'Players', route: 'PlayersScreen', onPress: () => navigation.navigate('PlayersScreen')}, route: "GameSettingsScreen",
{label: 'Leaderboard', route: 'LeaderboardScreen', onPress: () => navigation.navigate('LeaderboardScreen')}, onPress: () => navigation.navigate("GameSettingsScreen"),
},
{
label: "Map",
route: "MapScreen",
onPress: () => navigation.navigate("MapScreen"),
},
{
label: "Players",
route: "PlayersScreen",
onPress: () => navigation.navigate("PlayersScreen"),
},
{
label: "Leaderboard",
route: "LeaderboardScreen",
onPress: () => navigation.navigate("LeaderboardScreen"),
},
{
label: "Games",
route: "GameSelectionScreen",
onPress: () => navigation.navigate("GameSelectionScreen"),
},
]; ];
const playerTabs = [ const playerTabs = [
{label: 'Team', route: 'TeamScreen', onPress: () => navigation.navigate('TeamScreen')}, {
{label: 'Map', route: 'MapScreen', onPress: () => navigation.navigate('MapScreen')}, label: "Team",
{label: 'Leaderboard', route: 'LeaderboardScreen', onPress: () => navigation.navigate('LeaderboardScreen')}, route: "TeamScreen",
{label: 'Leave', route: '__leave__', onPress: () => handleLeaveGame()}, onPress: () => navigation.navigate("TeamScreen"),
},
{
label: "Map",
route: "MapScreen",
onPress: () => navigation.navigate("MapScreen"),
},
{
label: "Leaderboard",
route: "LeaderboardScreen",
onPress: () => navigation.navigate("LeaderboardScreen"),
},
{
label: "Games",
route: "GameSelectionScreen",
onPress: () => navigation.navigate("GameSelectionScreen"),
},
{ label: "Leave", route: "__leave__", onPress: () => handleLeaveGame() },
]; ];
const tabs = session.isAcceptedAdmin ? adminTabs : playerTabs; const tabs = session.isAcceptedAdmin ? adminTabs : playerTabs;
// State ---------------------- // State ----------------------
// Handlers ------------------- // Handlers -------------------
const handleLeaveGame = () => { const handleLeaveGame = () => {
Alert.alert('Leave Game', 'Are you sure you want to leave this game?', [ Alert.alert("Leave Game", "Are you sure you want to leave this game?", [
{text: 'Cancel', style: 'cancel'}, { text: "Cancel", style: "cancel" },
{ {
text: 'Leave', text: "Leave",
style: 'destructive', style: "destructive",
onPress: async () => { onPress: async () => {
const s = getSession(); const s = getSession();
const endpoint = `${API_BASE_URL}/subgroup-memberships?Uid=${s.currentUid}&Gid=${s.currentGid}`; const endpoint = `${API_BASE_URL}/subgroup-memberships?Uid=${s.currentUid}&Gid=${s.currentGid}`;
const res = await API.get(endpoint); const res = await API.get(endpoint);
if (res.isSuccess && res.result.length > 0) { if (res.isSuccess && res.result.length > 0) {
await API.delete(`${API_BASE_URL}/subgroup-memberships/${res.result[0].id}`); await API.delete(
`${API_BASE_URL}/subgroup-memberships/${res.result[0].id}`,
);
} }
clearGameSession(); clearGameSession();
navigation.reset({index: 0, routes: [{name: 'Game'}]}); navigation.reset({ index: 0, routes: [{ name: "Game" }] });
}, },
}, },
]); ]);
}; };
// View ----------------------- // View -----------------------
return ( return (
<View style={[styles.container, {paddingBottom: insets.bottom}]}> <View style={[styles.container, { paddingBottom: insets.bottom }]}>
{tabs.map((tab) => { {tabs.map((tab) => {
const isActive = routeName === tab.route; const isActive = routeName === tab.route;
const isLeave = tab.route === '__leave__'; const isLeave = tab.route === "__leave__";
return ( return (
<Pressable <Pressable
key={tab.label} key={tab.label}
onPress={tab.onPress} onPress={tab.onPress}
style={[styles.tab, isActive && styles.tabActive, isLeave && styles.tabLeave]} style={[
styles.tab,
isActive && styles.tabActive,
isLeave && styles.tabLeave,
]}
>
<Text
style={[
styles.tabText,
isActive && styles.tabTextActive,
isLeave && styles.tabTextLeave,
]}
> >
<Text style={[styles.tabText, isActive && styles.tabTextActive, isLeave && styles.tabTextLeave]}>
{tab.label} {tab.label}
</Text> </Text>
</Pressable> </Pressable>
@@ -75,38 +126,38 @@ const styles = StyleSheet.create({
container: { container: {
minHeight: 64, minHeight: 64,
borderTopWidth: 1, borderTopWidth: 1,
borderTopColor: '#555555', borderTopColor: "#555555",
backgroundColor: '#4a4a4a', backgroundColor: "#4a4a4a",
flexDirection: 'row', flexDirection: "row",
alignItems: 'center', alignItems: "center",
justifyContent: 'space-around', justifyContent: "space-around",
paddingHorizontal: 8, paddingHorizontal: 8,
}, },
tab: { tab: {
flex: 1, flex: 1,
alignItems: 'center', alignItems: "center",
justifyContent: 'center', justifyContent: "center",
paddingVertical: 12, paddingVertical: 12,
borderRadius: 8, borderRadius: 8,
marginHorizontal: 4, marginHorizontal: 4,
}, },
tabActive: { tabActive: {
backgroundColor: '#5c5c5c', backgroundColor: "#5c5c5c",
}, },
tabLeave: { tabLeave: {
backgroundColor: 'rgba(220,38,38,0.15)', backgroundColor: "rgba(220,38,38,0.15)",
}, },
tabText: { tabText: {
color: '#d1d5db', color: "#d1d5db",
fontSize: 12, fontSize: 12,
fontWeight: '600', fontWeight: "600",
textAlign: 'center', textAlign: "center",
}, },
tabTextActive: { tabTextActive: {
color: '#ffffff', color: "#ffffff",
}, },
tabTextLeave: { tabTextLeave: {
color: '#fca5a5', color: "#fca5a5",
}, },
}); });
+26 -9
View File
@@ -1,7 +1,7 @@
import {StatusBar} from "expo-status-bar"; import { StatusBar } from "expo-status-bar";
import {StyleSheet, View} from "react-native"; import { StyleSheet, View } from "react-native";
import {useNavigation, useRoute} from "@react-navigation/native"; import { useNavigation, useRoute } from "@react-navigation/native";
import {useSafeAreaInsets} from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import TopNavbar from "./TopNavbar"; import TopNavbar from "./TopNavbar";
import BottomNavbar from "./BottomNavbar"; import BottomNavbar from "./BottomNavbar";
@@ -17,6 +17,7 @@ const Screen = ({ children, style, showBack = false }) => {
TeamScreen: "Current Team", TeamScreen: "Current Team",
MapScreen: "Map", MapScreen: "Map",
GameSettingsScreen: "Game Settings", GameSettingsScreen: "Game Settings",
GameSelectionScreen: "Games",
PlayersScreen: "Players", PlayersScreen: "Players",
ExpandedMapScreen: "Map View", ExpandedMapScreen: "Map View",
}; };
@@ -24,17 +25,34 @@ const Screen = ({ children, style, showBack = false }) => {
// State ---------------------- // State ----------------------
const routeName = route.name; const routeName = route.name;
const isAuthScreen = routeName === "LoginScreen" || routeName === "RegisterScreen"; const isAuthScreen =
routeName === "LoginScreen" || routeName === "RegisterScreen";
const pageTitle = titleMap[routeName] || "Treasure Hunt"; const pageTitle = titleMap[routeName] || "Treasure Hunt";
// Handlers ------------------- // Handlers -------------------
// View ----------------------- // View -----------------------
return ( return (
<View style={[styles.screen, isAuthScreen && {paddingTop: insets.top, paddingBottom: insets.bottom}]}> <View
{!isAuthScreen && <TopNavbar title={pageTitle} showBack={showBack} navigation={navigation} />} style={[
styles.screen,
isAuthScreen && {
paddingTop: insets.top,
paddingBottom: insets.bottom,
},
]}
>
{!isAuthScreen && (
<TopNavbar
title={pageTitle}
showBack={showBack}
navigation={navigation}
/>
)}
<View style={[styles.content, style]}>{children}</View> <View style={[styles.content, style]}>{children}</View>
{!isAuthScreen && <BottomNavbar navigation={navigation} routeName={routeName} />} {!isAuthScreen && (
<BottomNavbar navigation={navigation} routeName={routeName} />
)}
<StatusBar style="light" /> <StatusBar style="light" />
</View> </View>
); );
@@ -52,4 +70,3 @@ const styles = StyleSheet.create({
}); });
export default Screen; export default Screen;
@@ -0,0 +1,184 @@
import { ScrollView, StyleSheet, Text, View } from "react-native";
import Screen from "../layout/Screen";
import Card from "../UI/Card";
import { Button, ButtonTray } from "../UI/Button";
import { getSession } from "../../hooks/SessionStore";
const GameSelectionScreen = ({ navigation }) => {
// Initialisations ---------------------
const session = getSession();
const isAdmin = Boolean(session.isAcceptedAdmin);
const privateGames = [
{
id: "active-game",
name: "Private Game",
players: "4 players",
endsIn: "Ends in 2h 15m",
},
];
// State -------------------------------
// Handlers ----------------------------
const handleCreateGame = () => {
if (isAdmin) {
navigation.navigate("GameSettingsScreen");
return;
}
navigation.navigate("TeamScreen");
};
const handleOpenGame = () => {
navigation.navigate("TeamScreen");
};
const handleExplore = () => {
navigation.navigate("MapScreen");
};
// View --------------------------------
return (
<Screen style={styles.screenContent}>
<ScrollView contentContainerStyle={styles.scrollContent}>
<View style={styles.sectionTop}>
<Text style={styles.sectionTitle}>Your Games</Text>
<ButtonTray>
<Button
label={isAdmin ? "Create Game" : "Manage Team"}
onClick={handleCreateGame}
styleButton={styles.primaryButton}
styleLabel={styles.primaryButtonLabel}
/>
</ButtonTray>
</View>
<View style={styles.sectionBlock}>
<Text style={styles.blockTitle}>Private Games</Text>
{privateGames.map((game) => (
<Card key={game.id} style={styles.gameCard}>
<Text style={styles.gameName}>{game.name}</Text>
<Text style={styles.gameMeta}>{game.players}</Text>
<Text style={styles.gameMeta}>{game.endsIn}</Text>
<View style={styles.cardActions}>
<Button
label="Show"
onClick={handleOpenGame}
styleButton={styles.secondaryButton}
styleLabel={styles.secondaryButtonLabel}
/>
</View>
</Card>
))}
</View>
<View style={styles.sectionBlock}>
<Text style={styles.blockTitle}>Global Mode</Text>
<Text style={styles.supportText}>
Explore all visible caches and live activity from the map.
</Text>
<ButtonTray>
<Button
label="Explore"
onClick={handleExplore}
styleButton={styles.neutralButton}
styleLabel={styles.neutralButtonLabel}
/>
</ButtonTray>
</View>
</ScrollView>
</Screen>
);
};
const styles = StyleSheet.create({
screenContent: {
backgroundColor: "#f3f4f6",
paddingHorizontal: 0,
paddingVertical: 0,
},
scrollContent: {
padding: 15,
paddingBottom: 28,
},
sectionTop: {
marginBottom: 16,
gap: 10,
},
sectionTitle: {
fontSize: 22,
fontWeight: "700",
color: "#111827",
},
sectionBlock: {
marginTop: 8,
marginBottom: 10,
},
blockTitle: {
fontSize: 15,
fontWeight: "700",
color: "#374151",
marginBottom: 8,
},
gameCard: {
marginBottom: 10,
paddingHorizontal: 16,
paddingVertical: 14,
borderColor: "#d1d5db",
borderRadius: 10,
},
gameName: {
fontSize: 17,
fontWeight: "700",
color: "#111827",
marginBottom: 4,
},
gameMeta: {
fontSize: 14,
color: "#4b5563",
marginBottom: 2,
},
cardActions: {
marginTop: 12,
maxWidth: 160,
},
supportText: {
fontSize: 14,
color: "#4b5563",
marginBottom: 10,
lineHeight: 20,
},
primaryButton: {
backgroundColor: "#16a34a",
borderColor: "#16a34a",
},
primaryButtonLabel: {
color: "#ffffff",
fontWeight: "600",
},
secondaryButton: {
backgroundColor: "#2563eb",
borderColor: "#2563eb",
flex: 0,
minHeight: 38,
paddingHorizontal: 18,
},
secondaryButtonLabel: {
color: "#ffffff",
fontWeight: "600",
fontSize: 14,
},
neutralButton: {
backgroundColor: "#374151",
borderColor: "#374151",
maxWidth: 180,
},
neutralButtonLabel: {
color: "#ffffff",
fontWeight: "600",
},
});
export default GameSelectionScreen;
+13
View File
@@ -0,0 +1,13 @@
/*import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
const Component = () => {
// Initialisations ---------------------
// State -------------------------------
// Handlers ----------------------------
// View --------------------------------
return (
);
};
const styles = StyleSheet.create({
});
export default Component;
*/