Add comments for code structure clarity; enhance readability across multiple components

This commit is contained in:
bobbert committed 2026-04-05 18:07:45 +01:00
1 parent d67522a71d
commit 9b9f13eb6a
29 files changed
+1020 -846

No files matched your search

+41 -50
View File
@@ -17,65 +17,56 @@ import ManageSubgroupsScreen from "./src/components/screens/ManageSubgroupsScree
const Stack = createNativeStackNavigator(); const Stack = createNativeStackNavigator();
const AuthStack = () => ( const AuthStack = () => (
<Stack.Navigator <Stack.Navigator
screenOptions={{ screenOptions={{headerShown: false}}
headerShown: false, >
}} <Stack.Screen name="LoginScreen" component={LoginScreen}/>
> <Stack.Screen name="RegisterScreen" component={RegisterScreen}/>
<Stack.Screen name="LoginScreen" component={LoginScreen} /> </Stack.Navigator>
<Stack.Screen name="RegisterScreen" component={RegisterScreen} />
</Stack.Navigator>
); );
const GameStack = () => ( const GameStack = () => (
<Stack.Navigator <Stack.Navigator
screenOptions={{ screenOptions={{headerShown: false}}
headerShown: false, >
}} <Stack.Screen name="GameTypeScreen" component={GameTypeScreen}/>
> <Stack.Screen name="JoinPrivateGameScreen" component={JoinPrivateGameScreen}/>
<Stack.Screen name="GameTypeScreen" component={GameTypeScreen} /> <Stack.Screen name="ManagePrivateGameScreen" component={ManagePrivateGameScreen}/>
<Stack.Screen name="JoinPrivateGameScreen" component={JoinPrivateGameScreen} /> <Stack.Screen name="CreateGameScreen" component={CreateGameScreen}/>
<Stack.Screen name="ManagePrivateGameScreen" component={ManagePrivateGameScreen} /> <Stack.Screen name="ManageGameScreen" component={ManageGameScreen}/>
<Stack.Screen name="CreateGameScreen" component={CreateGameScreen} /> <Stack.Screen name="GameLobbyScreen" component={GameLobbyScreen}/>
<Stack.Screen name="ManageGameScreen" component={ManageGameScreen} /> <Stack.Screen name="LeaderboardScreen" component={LeaderboardScreen}/>
<Stack.Screen name="GameLobbyScreen" component={GameLobbyScreen} /> <Stack.Screen name="TeamScreen" component={TeamScreen}/>
<Stack.Screen name="LeaderboardScreen" component={LeaderboardScreen} /> <Stack.Screen name="MapScreen" component={MapScreen}/>
<Stack.Screen name="TeamScreen" component={TeamScreen} /> </Stack.Navigator>
<Stack.Screen name="MapScreen" component={MapScreen} />
</Stack.Navigator>
); );
const AdminStack = () => ( const AdminStack = () => (
<Stack.Navigator <Stack.Navigator
screenOptions={{ screenOptions={{headerShown: false}}
headerShown: false, >
}} <Stack.Screen name="AdminHubScreen" component={AdminHubScreen}/>
> <Stack.Screen name="ManageSubgroupsScreen" component={ManageSubgroupsScreen}/>
<Stack.Screen name="AdminHubScreen" component={AdminHubScreen} /> <Stack.Screen name="LeaderboardScreen" component={LeaderboardScreen}/>
<Stack.Screen name="ManageSubgroupsScreen" component={ManageSubgroupsScreen} /> <Stack.Screen name="MapScreen" component={MapScreen}/>
<Stack.Screen name="LeaderboardScreen" component={LeaderboardScreen} /> <Stack.Screen name="TeamScreen" component={TeamScreen}/>
<Stack.Screen name="MapScreen" component={MapScreen} /> <Stack.Screen name="CreateGameScreen" component={CreateGameScreen}/>
<Stack.Screen name="TeamScreen" component={TeamScreen} /> <Stack.Screen name="ManageGameScreen" component={ManageGameScreen}/>
<Stack.Screen name="CreateGameScreen" component={CreateGameScreen} /> </Stack.Navigator>
<Stack.Screen name="ManageGameScreen" component={ManageGameScreen} />
</Stack.Navigator>
); );
export const App = () => { export const App = () => {
return ( return (
<NavigationContainer> <NavigationContainer>
<Stack.Navigator <Stack.Navigator
screenOptions={{ screenOptions={{headerShown: false}}
headerShown: false, >
}} <Stack.Screen name="Auth" component={AuthStack}/>
> <Stack.Screen name="Game" component={GameStack}/>
<Stack.Screen name="Auth" component={AuthStack} /> <Stack.Screen name="Admin" component={AdminStack}/>
<Stack.Screen name="Game" component={GameStack} /> </Stack.Navigator>
<Stack.Screen name="Admin" component={AdminStack} /> </NavigationContainer>
</Stack.Navigator> );
</NavigationContainer>
);
}; };
export default App; export default App;
-3
View File
@@ -3,10 +3,7 @@
"main": "node_modules/expo/AppEntry.js", "main": "node_modules/expo/AppEntry.js",
"version": "1.0.0", "version": "1.0.0",
"scripts": { "scripts": {
"api": "node server/server.js",
"api:standalone": "npm --prefix server start",
"start": "expo start", "start": "expo start",
"start:api": "npm run api",
"android": "expo run:android", "android": "expo run:android",
"ios": "expo run:ios", "ios": "expo run:ios",
"web": "expo start --web", "web": "expo start --web",
+32
View File
@@ -0,0 +1,32 @@
const API = {};
API.get = (endpoint) => callFetch(endpoint, 'GET');
API.post = (endpoint, data) => callFetch(endpoint, 'POST', data);
API.put = (endpoint, data) => callFetch(endpoint, 'PUT', data);
API.delete = (endpoint) => callFetch(endpoint, 'DELETE');
export default API;
const callFetch = async (endpoint, method, dataObj = null) => {
// Build request object
let requestObj = {method: method}; // GET, POST, PUT or DELETE
if (dataObj)
requestObj = {
...requestObj,
headers: {'Content-type': 'application/json'},
body: JSON.stringify(dataObj),
};
// Call the fetch and process the return
try {
let result = null;
const response = await fetch(endpoint, requestObj);
if (response.status !== 204) result = await response.json();
return response.status >= 200 && response.status < 300
? {isSuccess: true, result}
: {isSuccess: false, message: `${result.message}`};
} catch (error) {
return {isSuccess: false, message: error.message};
}
};
+30
View File
@@ -0,0 +1,30 @@
import {useEffect, useState} from "react";
import API from "./API";
const useLoad = (loadEndpoint) => {
// State ----------------------
const [records, setRecords] = useState([]);
const [isLoading, setIsLoading] = useState(true);
// Loader ----------------------
const loadRecords = async (endpoint) => {
setIsLoading(true);
const response = await API.get(endpoint);
if (response.isSuccess) setRecords(response.result);
setIsLoading(false);
};
useEffect(() => {
loadRecords(loadEndpoint);
}, []);
// return ----------------------
return [records, setRecords, isLoading, loadRecords];
};
export default useLoad;
+10
View File
@@ -1,6 +1,11 @@
import {Pressable, StyleSheet, Text, View} from "react-native"; import {Pressable, StyleSheet, Text, View} from "react-native";
export const Button = ({ label, icon, onClick, styleLabel, styleButton }) => { export const Button = ({ label, icon, onClick, styleLabel, styleButton }) => {
// Initialisation -------------
// State ----------------------
// Handlers -------------------
// View -----------------------
return ( return (
<Pressable onPress={onClick} style={[styles.button, styleButton]}> <Pressable onPress={onClick} style={[styles.button, styleButton]}>
{icon ? icon : null} {icon ? icon : null}
@@ -10,6 +15,11 @@ export const Button = ({ label, icon, onClick, styleLabel, styleButton }) => {
}; };
export const ButtonTray = ({ children }) => { export const ButtonTray = ({ children }) => {
// Initialisation -------------
// State ----------------------
// Handlers -------------------
// View -----------------------
return <View style={styles.buttonTray}>{children}</View>; return <View style={styles.buttonTray}>{children}</View>;
}; };
@@ -2,6 +2,11 @@ import React from 'react';
import MapView, {Circle, Marker} from 'react-native-maps'; import MapView, {Circle, Marker} from 'react-native-maps';
const AdminCacheEditorView = ({ userLocation, caches, onAddCache, onMoveCache }) => { const AdminCacheEditorView = ({ userLocation, caches, onAddCache, onMoveCache }) => {
// Initialisation -------------
// State ----------------------
// Handlers -------------------
// View -----------------------
if (!userLocation) { if (!userLocation) {
return null; return null;
} }
+7 -1
View File
@@ -2,8 +2,13 @@ import React, {useEffect, useState} from 'react';
import {StyleSheet, Text, View} from 'react-native'; import {StyleSheet, Text, View} from 'react-native';
const ClaimTimerView = ({ cache, onClaimSuccess, isClaiming }) => { const ClaimTimerView = ({ cache, onClaimSuccess, isClaiming }) => {
// Initialisation -------------
// State ----------------------
const [timeLeft, setTimeLeft] = useState(5); const [timeLeft, setTimeLeft] = useState(5);
// Handlers -------------------
useEffect(() => { useEffect(() => {
let timer; let timer;
if (isClaiming && timeLeft > 0) { if (isClaiming && timeLeft > 0) {
@@ -16,6 +21,8 @@ const ClaimTimerView = ({ cache, onClaimSuccess, isClaiming }) => {
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [isClaiming, timeLeft, cache, onClaimSuccess]); }, [isClaiming, timeLeft, cache, onClaimSuccess]);
// View -----------------------
if (!isClaiming) { if (!isClaiming) {
return null; return null;
} }
@@ -45,4 +52,3 @@ const styles = StyleSheet.create({
}); });
export default ClaimTimerView; export default ClaimTimerView;
+6 -3
View File
@@ -3,9 +3,12 @@ import MapView, {Circle} from 'react-native-maps';
import ClaimTimerView from './ClaimTimerView'; import ClaimTimerView from './ClaimTimerView';
const PlayerMapView = ({ userLocation, visibleCache, isClaiming, onClaimSuccess }) => { const PlayerMapView = ({ userLocation, visibleCache, isClaiming, onClaimSuccess }) => {
if (!userLocation) { // Initialisation -------------
return null; // State ----------------------
} // Handlers -------------------
// View -----------------------
if (!userLocation) return null;
return ( return (
<> <>
+69 -64
View File
@@ -1,74 +1,79 @@
import {Pressable, StyleSheet, Text, View} from 'react-native'; import {Pressable, StyleSheet, Text, View} from 'react-native';
const BottomNavbar = ({ navigation, routeName }) => { const BottomNavbar = ({ navigation, routeName }) => {
const tabs = [ // Initialisation ------------
{
label: 'Map',
route: 'MapScreen',
onPress: () => navigation.navigate('Game', { screen: 'MapScreen' }),
},
{
label: 'Leaderboard',
route: 'LeaderboardScreen',
onPress: () => navigation.navigate('Game', { screen: 'LeaderboardScreen' }),
},
{
label: 'Current Team',
route: 'TeamScreen',
onPress: () => navigation.navigate('Game', { screen: 'TeamScreen' }),
},
];
return ( const tabs = [
<View style={styles.container}> {
{tabs.map((tab) => { label: 'Map',
const isActive = routeName === tab.route; route: 'MapScreen',
return ( onPress: () => navigation.navigate('Game', { screen: 'MapScreen' }),
<Pressable },
key={tab.route} {
onPress={tab.onPress} label: 'Leaderboard',
style={[styles.tab, isActive && styles.tabActive]} route: 'LeaderboardScreen',
> onPress: () => navigation.navigate('Game', { screen: 'LeaderboardScreen' }),
<Text style={[styles.tabText, isActive && styles.tabTextActive]}>{tab.label}</Text> },
</Pressable> {
); label: 'Current Team',
})} route: 'TeamScreen',
</View> onPress: () => navigation.navigate('Game', { screen: 'TeamScreen' }),
); },
];
// State ----------------------
// Handlers -------------------
// View -----------------------
return (
<View style={styles.container}>
{tabs.map((tab) => {
const isActive = routeName === tab.route;
return (
<Pressable
key={tab.route}
onPress={tab.onPress}
style={[styles.tab, isActive && styles.tabActive]}
>
<Text style={[styles.tabText, isActive && styles.tabTextActive]}>{tab.label}</Text>
</Pressable>
);
})}
</View>
);
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
minHeight: 64, minHeight: 64,
borderTopWidth: 1, borderTopWidth: 1,
borderTopColor: '#e5e7eb', borderTopColor: '#e5e7eb',
backgroundColor: '#ffffff', backgroundColor: '#ffffff',
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: '#eff6ff', backgroundColor: '#eff6ff',
}, },
tabText: { tabText: {
color: '#374151', color: '#374151',
fontSize: 12, fontSize: 12,
fontWeight: '600', fontWeight: '600',
textAlign: 'center', textAlign: 'center',
}, },
tabTextActive: { tabTextActive: {
color: '#2563eb', color: '#2563eb',
}, },
}); });
export default BottomNavbar; export default BottomNavbar;
+12 -6
View File
@@ -5,18 +5,18 @@ import TopNavbar from "./TopNavbar";
import BottomNavbar from "./BottomNavbar"; import BottomNavbar from "./BottomNavbar";
const Screen = ({ children, style }) => { const Screen = ({ children, style }) => {
// Initialisation -------------
const navigation = useNavigation(); const navigation = useNavigation();
const route = useRoute(); const route = useRoute();
const titleMap = { const titleMap = {
LoginScreen: "Login",
RegisterScreen: "Register",
GameTypeScreen: "Game Type", GameTypeScreen: "Game Type",
JoinPrivateGameScreen: "Join Private Game", JoinPrivateGameScreen: "Join Private Game",
ManagePrivateGameScreen: "Manage Private Game", ManagePrivateGameScreen: "Manage Private Game",
CreateGameScreen: "Create Game", CreateGameScreen: "Create Game",
ManageGameScreen: "Manage Game", ManageGameScreen: "Manage Game",
GameLobbyScreen: "Leaderboard", GameLobbyScreen: "Game Lobby",
LeaderboardScreen: "Leaderboard", LeaderboardScreen: "Leaderboard",
TeamScreen: "Current Team", TeamScreen: "Current Team",
MapScreen: "Map", MapScreen: "Map",
@@ -24,15 +24,21 @@ const Screen = ({ children, style }) => {
ManageSubgroupsScreen: "Manage Subgroups", ManageSubgroupsScreen: "Manage Subgroups",
}; };
// State ----------------------
const routeName = route.name; const routeName = route.name;
const showBack = routeName !== "LoginScreen" && navigation.canGoBack(); const isAuthScreen = routeName === "LoginScreen" || routeName === "RegisterScreen";
const showBack = !isAuthScreen && navigation.canGoBack();
const pageTitle = titleMap[routeName] || "Treasure Hunt"; const pageTitle = titleMap[routeName] || "Treasure Hunt";
// Handlers -------------------
// View -----------------------
return ( return (
<View style={[styles.screen, style]}> <View style={[styles.screen, style]}>
<TopNavbar title={pageTitle} showBack={showBack} navigation={navigation} /> {!isAuthScreen && <TopNavbar title={pageTitle} showBack={showBack} navigation={navigation} />}
<View style={styles.content}>{children}</View> <View style={styles.content}>{children}</View>
<BottomNavbar navigation={navigation} routeName={routeName} /> {!isAuthScreen && <BottomNavbar navigation={navigation} routeName={routeName} />}
<StatusBar style="light" /> <StatusBar style="light" />
</View> </View>
); );
+82 -78
View File
@@ -1,90 +1,94 @@
import {Pressable, StyleSheet, Text, View} from 'react-native'; import {Pressable, StyleSheet, Text, View} from 'react-native';
import {clearSession} from '../../hooks/SessionStore'; import {clearSession} from '../../hooks/SessionStore';
const TopNavbar = ({ title, showBack, navigation }) => { const TopNavbar = ({title, showBack, navigation}) => {
const routeName = navigation.getState()?.routes?.[navigation.getState().index]?.name; // Initialisation ------------
const showLogout = routeName !== 'LoginScreen' && routeName !== 'RegisterScreen';
const handleBack = () => { const routeName = navigation.getState()?.routes?.[navigation.getState().index]?.name;
if (navigation.canGoBack()) { const showLogout = routeName !== 'LoginScreen' && routeName !== 'RegisterScreen';
navigation.goBack();
}
};
const handleLogout = () => { // State ----------------------
clearSession(); // Handlers -------------------
navigation.reset({
index: 0,
routes: [{ name: 'Auth', params: { screen: 'LoginScreen' } }],
});
};
return ( const handleBack = () => {
<View style={styles.container}> if (navigation.canGoBack()) navigation.goBack();
<View style={styles.leftSlot}> };
{showBack ? (
<Pressable onPress={handleBack} style={styles.iconButton}> const handleLogout = () => {
<Text style={styles.iconText}>{'<'}</Text> clearSession();
</Pressable> navigation.reset({
) : null} index: 0,
</View> routes: [{name: 'Auth', params: {screen: 'LoginScreen'}}],
<Text style={styles.title}>{title}</Text> });
<View style={styles.rightSlot}> };
{showLogout ? (
<Pressable onPress={handleLogout} style={styles.logoutButton}> // View -----------------------
<Text style={styles.logoutText}>Logout</Text>
</Pressable> return (
) : null} <View style={styles.container}>
</View> <View style={styles.leftSlot}>
</View> {showBack ? (
); <Pressable onPress={handleBack} style={styles.iconButton}>
<Text style={styles.iconText}>{'<'}</Text>
</Pressable>
) : null}
</View>
<Text style={styles.title}>{title}</Text>
<View style={styles.rightSlot}>
{showLogout ? (
<Pressable onPress={handleLogout} style={styles.logoutButton}>
<Text style={styles.logoutText}>Logout</Text>
</Pressable>
) : null}
</View>
</View>
);
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
minHeight: 56, minHeight: 56,
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: '#e5e7eb', borderBottomColor: '#e5e7eb',
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
paddingHorizontal: 12, paddingHorizontal: 12,
backgroundColor: '#ffffff', backgroundColor: '#ffffff',
}, },
leftSlot: { leftSlot: {
width: 72, width: 72,
alignItems: 'flex-start', alignItems: 'flex-start',
}, },
rightSlot: { rightSlot: {
width: 72, width: 72,
alignItems: 'flex-end', alignItems: 'flex-end',
}, },
iconButton: { iconButton: {
paddingVertical: 8, paddingVertical: 8,
paddingHorizontal: 10, paddingHorizontal: 10,
}, },
iconText: { iconText: {
fontSize: 20, fontSize: 20,
color: '#111827', color: '#111827',
fontWeight: '700', fontWeight: '700',
}, },
title: { title: {
flex: 1, flex: 1,
textAlign: 'center', textAlign: 'center',
fontSize: 18, fontSize: 18,
fontWeight: '700', fontWeight: '700',
color: '#111827', color: '#111827',
}, },
logoutButton: { logoutButton: {
paddingVertical: 6, paddingVertical: 6,
paddingHorizontal: 8, paddingHorizontal: 8,
}, },
logoutText: { logoutText: {
color: '#2563eb', color: '#2563eb',
fontSize: 13, fontSize: 13,
fontWeight: '600', fontWeight: '600',
}, },
}); });
export default TopNavbar; export default TopNavbar;
+16 -9
View File
@@ -2,17 +2,24 @@ import React from 'react';
import {StyleSheet, Text} from 'react-native'; import {StyleSheet, Text} from 'react-native';
import Screen from '../layout/Screen'; import Screen from '../layout/Screen';
const AdminHubScreen = () => ( const AdminHubScreen = () => {
<Screen style={styles.container}> // Initialisation -------------
<Text style={styles.title}>Admin Hub</Text> // State ----------------------
<Text style={styles.body}>Admin hub setup goes here.</Text> // Handlers -------------------
</Screen> // View -----------------------
);
return (
<Screen style={styles.container}>
<Text style={styles.title}>Admin Hub</Text>
<Text style={styles.body}>Admin hub setup goes here.</Text>
</Screen>
);
};
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { justifyContent: 'center', alignItems: 'center' }, container: {justifyContent: 'center', alignItems: 'center'},
title: { fontSize: 24, fontWeight: 'bold' }, title: {fontSize: 24, fontWeight: 'bold'},
body: { color: '#4b5563', fontSize: 15, marginTop: 10 }, body: {color: '#4b5563', fontSize: 15, marginTop: 10},
}); });
export default AdminHubScreen; export default AdminHubScreen;
+16 -9
View File
@@ -2,17 +2,24 @@ import React from 'react';
import {StyleSheet, Text} from 'react-native'; import {StyleSheet, Text} from 'react-native';
import Screen from '../layout/Screen'; import Screen from '../layout/Screen';
const CreateGameScreen = () => ( const CreateGameScreen = () => {
<Screen style={styles.container}> // Initialisation -------------
<Text style={styles.title}>Create Game</Text> // State ----------------------
<Text style={styles.body}>Create game setup goes here.</Text> // Handlers -------------------
</Screen> // View -----------------------
);
return (
<Screen style={styles.container}>
<Text style={styles.title}>Create Game</Text>
<Text style={styles.body}>Create game setup goes here.</Text>
</Screen>
);
};
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { justifyContent: 'center', alignItems: 'center' }, container: {justifyContent: 'center', alignItems: 'center'},
title: { fontSize: 24, fontWeight: 'bold' }, title: {fontSize: 24, fontWeight: 'bold'},
body: { color: '#4b5563', fontSize: 15, marginTop: 10 }, body: {color: '#4b5563', fontSize: 15, marginTop: 10},
}); });
export default CreateGameScreen; export default CreateGameScreen;
+16 -9
View File
@@ -2,17 +2,24 @@ import React from 'react';
import {StyleSheet, Text} from 'react-native'; import {StyleSheet, Text} from 'react-native';
import Screen from '../layout/Screen'; import Screen from '../layout/Screen';
const GameLobbyScreen = () => ( const GameLobbyScreen = () => {
<Screen style={styles.container}> // Initialisation -------------
<Text style={styles.title}>Game Lobby</Text> // State ----------------------
<Text style={styles.body}>Game lobby setup goes here.</Text> // Handlers -------------------
</Screen> // View -----------------------
);
return (
<Screen style={styles.container}>
<Text style={styles.title}>Game Lobby</Text>
<Text style={styles.body}>Game lobby setup goes here.</Text>
</Screen>
);
};
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { justifyContent: 'center', alignItems: 'center' }, container: {justifyContent: 'center', alignItems: 'center'},
title: { fontSize: 24, fontWeight: 'bold' }, title: {fontSize: 24, fontWeight: 'bold'},
body: { color: '#4b5563', fontSize: 15, marginTop: 10 }, body: {color: '#4b5563', fontSize: 15, marginTop: 10},
}); });
export default GameLobbyScreen; export default GameLobbyScreen;
+57 -52
View File
@@ -3,66 +3,71 @@ import {Pressable, StyleSheet, Text, View} from 'react-native';
import useGameHook from '../../hooks/useGameHook'; import useGameHook from '../../hooks/useGameHook';
import Screen from '../layout/Screen'; import Screen from '../layout/Screen';
const GameTypeScreen = ({ navigation }) => { const GameTypeScreen = ({navigation}) => {
const { getGameTypes } = useGameHook(); // Initialisation ------------
const [gameTypes, setGameTypes] = useState([]);
useEffect(() => { const {getGameTypes} = useGameHook();
let isMounted = true;
const load = async () => { // State ----------------------
const types = await getGameTypes();
if (isMounted) {
setGameTypes(types || []);
}
};
load(); const [gameTypes, setGameTypes] = useState([]);
return () => { // Handlers -------------------
isMounted = false;
};
}, [getGameTypes]);
return ( useEffect(() => {
<Screen style={styles.container}> let isMounted = true;
<Text style={styles.title}>Select Game Type</Text> const load = async () => {
<View style={styles.content}> const types = await getGameTypes();
{gameTypes && gameTypes.map((type) => ( if (isMounted) setGameTypes(types || []);
<Pressable key={type} style={styles.button} onPress={() => navigation.navigate('GameLobbyScreen')}> };
<Text style={styles.buttonText}>{type}</Text> load();
</Pressable> return () => { isMounted = false; };
))} }, []);
</View>
</Screen> // View -----------------------
);
return (
<Screen style={styles.container}>
<Text style={styles.title}>Select Game Type</Text>
<View style={styles.content}>
{gameTypes && gameTypes.map((type) => (
<Pressable
key={type}
style={styles.button}
onPress={() => navigation.navigate('GameLobbyScreen')}
>
<Text style={styles.buttonText}>{type}</Text>
</Pressable>
))}
</View>
</Screen>
);
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
justifyContent: 'center', justifyContent: 'center',
}, },
content: { content: {
gap: 12, gap: 12,
flex: 1, flex: 1,
}, },
title: { title: {
fontSize: 24, fontSize: 24,
fontWeight: 'bold', fontWeight: 'bold',
textAlign: 'center', textAlign: 'center',
marginBottom: 20, marginBottom: 20,
}, },
button: { button: {
backgroundColor: '#2563eb', backgroundColor: '#2563eb',
borderRadius: 8, borderRadius: 8,
paddingVertical: 12, paddingVertical: 12,
alignItems: 'center', alignItems: 'center',
}, },
buttonText: { buttonText: {
color: '#ffffff', color: '#ffffff',
fontWeight: '600', fontWeight: '600',
}, },
}); });
export default GameTypeScreen; export default GameTypeScreen;
@@ -3,6 +3,11 @@ import {StyleSheet, Text} from 'react-native';
import Screen from '../layout/Screen'; import Screen from '../layout/Screen';
const JoinPrivateGameScreen = () => { const JoinPrivateGameScreen = () => {
// Initialisation -------------
// State ----------------------
// Handlers -------------------
// View -----------------------
return ( return (
<Screen style={styles.container}> <Screen style={styles.container}>
<Text style={styles.title}>Join Private Game</Text> <Text style={styles.title}>Join Private Game</Text>
+16 -10
View File
@@ -2,18 +2,24 @@ import React from 'react';
import {StyleSheet, Text} from 'react-native'; import {StyleSheet, Text} from 'react-native';
import Screen from '../layout/Screen'; import Screen from '../layout/Screen';
const LeaderboardScreen = () => ( const LeaderboardScreen = () => {
<Screen style={styles.container}> // Initialisation -------------
<Text style={styles.title}>Leaderboard</Text> // State ----------------------
<Text style={styles.body}>Leaderboard view goes here.</Text> // Handlers -------------------
</Screen> // View -----------------------
);
return (
<Screen style={styles.container}>
<Text style={styles.title}>Leaderboard</Text>
<Text style={styles.body}>Leaderboard view goes here.</Text>
</Screen>
);
};
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { justifyContent: 'center', alignItems: 'center' }, container: {justifyContent: 'center', alignItems: 'center'},
title: { fontSize: 24, fontWeight: 'bold' }, title: {fontSize: 24, fontWeight: 'bold'},
body: { color: '#4b5563', fontSize: 15, marginTop: 10 }, body: {color: '#4b5563', fontSize: 15, marginTop: 10},
}); });
export default LeaderboardScreen; export default LeaderboardScreen;
+9 -1
View File
@@ -5,9 +5,16 @@ import Screen from '../layout/Screen';
import {Button, ButtonTray} from '../UI/Button'; import {Button, ButtonTray} from '../UI/Button';
const LoginScreen = ({ navigation }) => { const LoginScreen = ({ navigation }) => {
// Initialisation ------------->
const { email, password, error, isLoading, setEmail, setPassword, login } = useAuthHook(); const { email, password, error, isLoading, setEmail, setPassword, login } = useAuthHook();
// State ----------------------
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
// Handlers -------------------
const handleLogin = async () => { const handleLogin = async () => {
setSubmitting(true); setSubmitting(true);
const user = await login(); const user = await login();
@@ -22,6 +29,8 @@ const LoginScreen = ({ navigation }) => {
navigation.reset({ index: 0, routes: [{ name: 'Game' }] }); navigation.reset({ index: 0, routes: [{ name: 'Game' }] });
}; };
// View -----------------------
return ( return (
<Screen style={styles.container}> <Screen style={styles.container}>
<View style={styles.content}> <View style={styles.content}>
@@ -107,4 +116,3 @@ const styles = StyleSheet.create({
}); });
export default LoginScreen; export default LoginScreen;
+110 -135
View File
@@ -7,163 +7,138 @@ import useGameHook from '../../hooks/useGameHook';
import {getSession} from '../../hooks/SessionStore'; import {getSession} from '../../hooks/SessionStore';
const ManageGameScreen = () => { const ManageGameScreen = () => {
const session = getSession(); // Initialisation ------------
const { getCaches, upsertCache } = useGameHook();
const [userLocation, setUserLocation] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [cacheRecords, setCacheRecords] = useState([]);
const loadCaches = useCallback(async () => { const session = getSession();
if (!session.currentGid) { const {getCaches, upsertCache} = useGameHook();
setCacheRecords([]);
return;
}
const rows = await getCaches(session.currentGid, null);
setCacheRecords(rows || []);
}, [getCaches, session.currentGid]);
useEffect(() => { // State ----------------------
loadCaches();
}, [loadCaches]);
useEffect(() => { const [userLocation, setUserLocation] = useState(null);
let locationSub; const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [cacheRecords, setCacheRecords] = useState([]);
const start = async () => { // Handlers -------------------
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
setError('Location permission denied');
setLoading(false);
return;
}
const current = await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.Balanced }); const loadCaches = useCallback(async () => {
setUserLocation({ latitude: current.coords.latitude, longitude: current.coords.longitude }); if (!session.currentGid) {
setCacheRecords([]);
return;
}
const rows = await getCaches(session.currentGid, null);
setCacheRecords(rows || []);
}, [session.currentGid]);
locationSub = await Location.watchPositionAsync( useEffect(() => { loadCaches(); }, [loadCaches]);
{
accuracy: Location.Accuracy.Balanced,
distanceInterval: 1,
timeInterval: 1000,
},
(next) => {
setUserLocation({ latitude: next.coords.latitude, longitude: next.coords.longitude });
},
);
setLoading(false); useEffect(() => {
let locationSub;
const start = async () => {
const {status} = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
setError('Location permission denied');
setLoading(false);
return;
}
const current = await Location.getCurrentPositionAsync({accuracy: Location.Accuracy.Balanced});
setUserLocation({latitude: current.coords.latitude, longitude: current.coords.longitude});
locationSub = await Location.watchPositionAsync(
{accuracy: Location.Accuracy.Balanced, distanceInterval: 1, timeInterval: 1000},
(next) => {
setUserLocation({latitude: next.coords.latitude, longitude: next.coords.longitude});
},
);
setLoading(false);
};
start();
return () => { if (locationSub) locationSub.remove(); };
}, []);
const handleAddCache = async (coordinate) => {
if (!session.currentGid) return;
await upsertCache({
gid: session.currentGid,
latitude: coordinate.latitude,
longitude: coordinate.longitude,
radius: 20,
clue: `Cache ${cacheRecords.length + 1}`,
subgroupId: 1,
});
await loadCaches();
}; };
start(); const handleMoveCache = async (cacheId, coordinate) => {
if (!session.currentGid) return;
return () => { const current = cacheRecords.find((cache) => cache.id === cacheId);
if (locationSub) { if (!current) return;
locationSub.remove(); await upsertCache({
} gid: session.currentGid,
cacheId,
latitude: coordinate.latitude,
longitude: coordinate.longitude,
radius: current.radius,
clue: current.clue,
subgroupId: current.subgroupId,
});
await loadCaches();
}; };
}, []);
const handleAddCache = async (coordinate) => { // View -----------------------
if (!session.currentGid) { if (!session.currentGid) {
return; return (
<Screen style={styles.center}>
<Text style={styles.body}>Create or join a game first.</Text>
</Screen>
);
} }
await upsertCache({
gid: session.currentGid,
latitude: coordinate.latitude,
longitude: coordinate.longitude,
radius: 20,
clue: `Cache ${cacheRecords.length + 1}`,
subgroupId: 1,
});
await loadCaches();
};
const handleMoveCache = async (cacheId, coordinate) => { if (!session.isAcceptedAdmin) {
if (!session.currentGid) { return (
return; <Screen style={styles.center}>
<Text style={styles.body}>Only admins can edit cache locations.</Text>
</Screen>
);
} }
const current = cacheRecords.find((cache) => cache.id === cacheId);
if (!current) { if (loading) {
return; return (
<Screen style={styles.center}>
<ActivityIndicator size="large"/>
</Screen>
);
}
if (error) {
return (
<Screen style={styles.center}>
<Text style={styles.error}>{error}</Text>
</Screen>
);
} }
await upsertCache({
gid: session.currentGid,
cacheId,
latitude: coordinate.latitude,
longitude: coordinate.longitude,
radius: current.radius,
clue: current.clue,
subgroupId: current.subgroupId,
});
await loadCaches();
};
if (!session.currentGid) {
return ( return (
<Screen style={styles.center}> <Screen style={styles.container}>
<Text style={styles.body}>Create or join a game first.</Text> <View style={styles.mapWrap}>
</Screen> <AdminCacheEditorView
userLocation={userLocation}
caches={cacheRecords}
onAddCache={handleAddCache}
onMoveCache={handleMoveCache}
/>
</View>
</Screen>
); );
}
if (!session.isAcceptedAdmin) {
return (
<Screen style={styles.center}>
<Text style={styles.body}>Only admins can edit cache locations.</Text>
</Screen>
);
}
if (loading) {
return (
<Screen style={styles.center}>
<ActivityIndicator size="large" />
</Screen>
);
}
if (error) {
return (
<Screen style={styles.center}>
<Text style={styles.error}>{error}</Text>
</Screen>
);
}
return (
<Screen style={styles.container}>
<View style={styles.mapWrap}>
<AdminCacheEditorView
userLocation={userLocation}
caches={cacheRecords}
onAddCache={handleAddCache}
onMoveCache={handleMoveCache}
/>
</View>
</Screen>
);
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {padding: 0},
padding: 0, mapWrap: {flex: 1},
}, center: {justifyContent: 'center', alignItems: 'center'},
mapWrap: { body: {color: '#4b5563', fontSize: 15},
flex: 1, error: {color: '#dc2626', fontSize: 15},
},
center: {
justifyContent: 'center',
alignItems: 'center',
},
body: {
color: '#4b5563',
fontSize: 15,
},
error: {
color: '#dc2626',
fontSize: 15,
},
}); });
export default ManageGameScreen; export default ManageGameScreen;
@@ -3,18 +3,23 @@ import {StyleSheet, Text} from 'react-native';
import Screen from '../layout/Screen'; import Screen from '../layout/Screen';
const ManagePrivateGameScreen = () => { const ManagePrivateGameScreen = () => {
return ( // Initialisation -------------
<Screen style={styles.container}> // State ----------------------
<Text style={styles.title}>Manage Private Game</Text> // Handlers -------------------
<Text style={styles.body}>Manage private game setup goes here.</Text> // View -----------------------
</Screen>
); return (
<Screen style={styles.container}>
<Text style={styles.title}>Manage Private Game</Text>
<Text style={styles.body}>Manage private game setup goes here.</Text>
</Screen>
);
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { justifyContent: 'center', alignItems: 'center' }, container: {justifyContent: 'center', alignItems: 'center'},
title: { fontSize: 24, fontWeight: 'bold' }, title: {fontSize: 24, fontWeight: 'bold'},
body: { color: '#4b5563', fontSize: 15, marginTop: 10 }, body: {color: '#4b5563', fontSize: 15, marginTop: 10},
}); });
export default ManagePrivateGameScreen; export default ManagePrivateGameScreen;
@@ -2,17 +2,24 @@ import React from 'react';
import {StyleSheet, Text} from 'react-native'; import {StyleSheet, Text} from 'react-native';
import Screen from '../layout/Screen'; import Screen from '../layout/Screen';
const ManageSubgroupsScreen = () => ( const ManageSubgroupsScreen = () => {
<Screen style={styles.container}> // Initialisation -------------
<Text style={styles.title}>Manage Subgroups</Text> // State ----------------------
<Text style={styles.body}>Manage subgroups setup goes here.</Text> // Handlers -------------------
</Screen> // View -----------------------
);
return (
<Screen style={styles.container}>
<Text style={styles.title}>Manage Subgroups</Text>
<Text style={styles.body}>Manage subgroups setup goes here.</Text>
</Screen>
);
};
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { justifyContent: 'center', alignItems: 'center' }, container: {justifyContent: 'center', alignItems: 'center'},
title: { fontSize: 24, fontWeight: 'bold' }, title: {fontSize: 24, fontWeight: 'bold'},
body: { color: '#4b5563', fontSize: 15, marginTop: 10 }, body: {color: '#4b5563', fontSize: 15, marginTop: 10},
}); });
export default ManageSubgroupsScreen; export default ManageSubgroupsScreen;
+123 -145
View File
@@ -9,175 +9,153 @@ import usePlayerGame from '../../hooks/usePlayerGame';
import {getSession} from '../../hooks/SessionStore'; import {getSession} from '../../hooks/SessionStore';
import Cache from '../../models/Cache'; import Cache from '../../models/Cache';
const toHeading = ({ x, y }) => { const toHeading = ({x, y}) => {
const angle = Math.atan2(y, x) * (180 / Math.PI); const angle = Math.atan2(y, x) * (180 / Math.PI);
return angle < 0 ? angle + 360 : angle; return angle < 0 ? angle + 360 : angle;
}; };
const MapScreen = () => { const MapScreen = () => {
const session = getSession(); // Initialisation ------------
const { getCaches, claimCache } = useGameHook();
const [userLocation, setUserLocation] = useState(null);
const [heading, setHeading] = useState(null);
const [loading, setLoading] = useState(true);
const [cacheRecords, setCacheRecords] = useState([]);
const [error, setError] = useState('');
const activeCaches = useMemo(() => ( const session = getSession();
cacheRecords.map( const {getCaches, claimCache} = useGameHook();
(cache) => new Cache(
cache.id,
cache.coordinates.latitude,
cache.coordinates.longitude,
cache.radius,
cache.clue,
cache.groupId,
cache.subgroupId,
),
)
), [cacheRecords]);
const subgroupFilter = session.currentSGid; // State ----------------------
const { visibleCache, isClaiming, setIsClaiming } = usePlayerGame(userLocation, heading, activeCaches);
const loadCaches = useCallback(async () => { const [userLocation, setUserLocation] = useState(null);
if (!session.currentGid) { const [heading, setHeading] = useState(null);
setCacheRecords([]); const [loading, setLoading] = useState(true);
return; const [cacheRecords, setCacheRecords] = useState([]);
} const [error, setError] = useState('');
const rows = await getCaches(session.currentGid, subgroupFilter);
setCacheRecords(rows || []);
}, [getCaches, session.currentGid, subgroupFilter]);
useEffect(() => { const activeCaches = useMemo(() => (
loadCaches(); cacheRecords.map((cache) => new Cache(
}, [loadCaches]); cache.id,
cache.coordinates.latitude,
cache.coordinates.longitude,
cache.radius,
cache.clue,
cache.groupId,
cache.subgroupId,
))
), [cacheRecords]);
useEffect(() => { const subgroupFilter = session.currentSGid;
let locationSub; const {visibleCache, isClaiming, setIsClaiming} = usePlayerGame(userLocation, heading, activeCaches);
let headingSub;
const start = async () => { // Handlers -------------------
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
setError('Location permission denied');
setLoading(false);
return;
}
const current = await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.Balanced }); const loadCaches = useCallback(async () => {
setUserLocation({ latitude: current.coords.latitude, longitude: current.coords.longitude }); if (!session.currentGid) {
setCacheRecords([]);
return;
}
const rows = await getCaches(session.currentGid, subgroupFilter);
setCacheRecords(rows || []);
}, [session.currentGid, subgroupFilter]);
locationSub = await Location.watchPositionAsync( useEffect(() => { loadCaches(); }, [loadCaches]);
{
accuracy: Location.Accuracy.Balanced,
distanceInterval: 1,
timeInterval: 1000,
},
(next) => {
setUserLocation({ latitude: next.coords.latitude, longitude: next.coords.longitude });
},
);
Magnetometer.setUpdateInterval(500); useEffect(() => {
headingSub = Magnetometer.addListener((data) => { let locationSub;
setHeading(toHeading(data)); let headingSub;
});
setLoading(false); const start = async () => {
const {status} = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
setError('Location permission denied');
setLoading(false);
return;
}
const current = await Location.getCurrentPositionAsync({accuracy: Location.Accuracy.Balanced});
setUserLocation({latitude: current.coords.latitude, longitude: current.coords.longitude});
locationSub = await Location.watchPositionAsync(
{accuracy: Location.Accuracy.Balanced, distanceInterval: 1, timeInterval: 1000},
(next) => {
setUserLocation({latitude: next.coords.latitude, longitude: next.coords.longitude});
},
);
Magnetometer.setUpdateInterval(500);
headingSub = Magnetometer.addListener((data) => { setHeading(toHeading(data)); });
setLoading(false);
};
start();
return () => {
if (locationSub) locationSub.remove();
if (headingSub) headingSub.remove();
};
}, []);
const handleClaim = async (cacheId) => {
if (!session.currentGid) return;
await claimCache({
gid: session.currentGid,
cacheId,
uid: session.currentUid,
tid: session.currentTid,
});
setIsClaiming(false);
await loadCaches();
}; };
start(); // View -----------------------
return () => {
if (locationSub) {
locationSub.remove();
}
if (headingSub) {
headingSub.remove();
}
};
}, []);
const handleClaim = async (cacheId) => {
if (!session.currentGid) { if (!session.currentGid) {
return; return (
<Screen style={styles.center}>
<Text style={styles.body}>Join or create a game to use map features.</Text>
</Screen>
);
}
if (session.isAcceptedAdmin) {
return (
<Screen style={styles.center}>
<Text style={styles.body}>Admins manage caches from Manage Game.</Text>
</Screen>
);
}
if (loading) {
return (
<Screen style={styles.center}>
<ActivityIndicator size="large"/>
</Screen>
);
}
if (error) {
return (
<Screen style={styles.center}>
<Text style={styles.error}>{error}</Text>
</Screen>
);
} }
await claimCache({
gid: session.currentGid,
cacheId,
uid: session.currentUid,
tid: session.currentTid,
});
setIsClaiming(false);
await loadCaches();
};
if (!session.currentGid) {
return ( return (
<Screen style={styles.center}> <Screen style={styles.container}>
<Text style={styles.body}>Join or create a game to use map features.</Text> <View style={styles.mapWrap}>
</Screen> <PlayerMapView
userLocation={userLocation}
visibleCache={visibleCache}
isClaiming={isClaiming}
onClaimSuccess={handleClaim}
/>
</View>
</Screen>
); );
}
if (session.isAcceptedAdmin) {
return (
<Screen style={styles.center}>
<Text style={styles.body}>Admins manage caches from Manage Game.</Text>
</Screen>
);
}
if (loading) {
return (
<Screen style={styles.center}>
<ActivityIndicator size="large" />
</Screen>
);
}
if (error) {
return (
<Screen style={styles.center}>
<Text style={styles.error}>{error}</Text>
</Screen>
);
}
return (
<Screen style={styles.container}>
<View style={styles.mapWrap}>
<PlayerMapView
userLocation={userLocation}
visibleCache={visibleCache}
isClaiming={isClaiming}
onClaimSuccess={handleClaim}
/>
</View>
</Screen>
);
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {padding: 0},
padding: 0, mapWrap: {flex: 1},
}, center: {justifyContent: 'center', alignItems: 'center'},
mapWrap: { body: {color: '#4b5563', fontSize: 15},
flex: 1, error: {color: '#dc2626', fontSize: 15},
},
center: {
justifyContent: 'center',
alignItems: 'center',
},
body: {
color: '#4b5563',
fontSize: 15,
},
error: {
color: '#dc2626',
fontSize: 15,
},
}); });
export default MapScreen; export default MapScreen;
+161 -159
View File
@@ -4,175 +4,177 @@ import useAuthHook from '../../hooks/useAuthHook';
import Screen from '../layout/Screen'; import Screen from '../layout/Screen';
import {Button, ButtonTray} from '../UI/Button'; import {Button, ButtonTray} from '../UI/Button';
const RegisterScreen = ({ navigation }) => { const RegisterScreen = ({navigation}) => {
const { // Initialisation ------------
username,
email,
password,
confirmPassword,
accountType,
error,
isLoading,
setUsername,
setEmail,
setPassword,
setConfirmPassword,
setAccountType,
register,
} = useAuthHook();
const [submitting, setSubmitting] = useState(false);
const handleRegister = async () => { const {
setSubmitting(true); username, email, password, confirmPassword, accountType,
const user = await register(); error, isLoading,
setSubmitting(false); setUsername, setEmail, setPassword, setConfirmPassword, setAccountType,
if (!user) { register,
return; } = useAuthHook();
}
if (user.IsAcceptedAdmin) {
navigation.reset({ index: 0, routes: [{ name: 'Admin' }] });
return;
}
navigation.reset({ index: 0, routes: [{ name: 'Game' }] });
};
return ( // State ----------------------
<Screen style={styles.container}>
<View style={styles.content}>
<Text style={styles.title}>Create Account</Text>
<TextInput const [submitting, setSubmitting] = useState(false);
style={styles.input}
placeholder="Username"
value={username}
onChangeText={setUsername}
editable={!isLoading}
/>
<TextInput
style={styles.input}
placeholder="Email"
autoCapitalize="none"
keyboardType="email-address"
value={email}
onChangeText={setEmail}
editable={!isLoading}
/>
<TextInput
style={styles.input}
placeholder="Password"
secureTextEntry
value={password}
onChangeText={setPassword}
editable={!isLoading}
/>
<TextInput
style={styles.input}
placeholder="Confirm Password"
secureTextEntry
value={confirmPassword}
onChangeText={setConfirmPassword}
editable={!isLoading}
/>
<View style={styles.typeRow}> // Handlers -------------------
<Pressable
style={[styles.typeButton, accountType === 'Individual' && styles.typeButtonActive]}
onPress={() => setAccountType('Individual')}
disabled={isLoading}
>
<Text style={[styles.typeText, accountType === 'Individual' && styles.typeTextActive]}>Individual</Text>
</Pressable>
<Pressable
style={[styles.typeButton, accountType === 'Business/School' && styles.typeButtonActive]}
onPress={() => setAccountType('Business/School')}
disabled={isLoading}
>
<Text style={[styles.typeText, accountType === 'Business/School' && styles.typeTextActive]}>Business</Text>
</Pressable>
</View>
{error ? <Text style={styles.errorText}>{error}</Text> : null} const handleRegister = async () => {
setSubmitting(true);
const user = await register();
setSubmitting(false);
if (!user) return;
if (user.IsAcceptedAdmin) {
navigation.reset({index: 0, routes: [{name: 'Admin'}]});
return;
}
navigation.reset({index: 0, routes: [{name: 'Game'}]});
};
<ButtonTray> // View -----------------------
<Button
label={submitting ? 'Creating...' : 'Create Account'}
onClick={handleRegister}
styleButton={styles.submitButton}
styleLabel={styles.submitText}
/>
</ButtonTray>
<Pressable onPress={() => navigation.navigate('Auth', { screen: 'LoginScreen' })}> return (
<Text style={styles.linkText}>Back to Login</Text> <Screen style={styles.container}>
</Pressable> <View style={styles.content}>
</View> <Text style={styles.title}>Create Account</Text>
</Screen>
); <TextInput
style={styles.input}
placeholder="Username"
value={username}
onChangeText={setUsername}
editable={!isLoading}
/>
<TextInput
style={styles.input}
placeholder="Email"
autoCapitalize="none"
keyboardType="email-address"
value={email}
onChangeText={setEmail}
editable={!isLoading}
/>
<TextInput
style={styles.input}
placeholder="Password"
secureTextEntry
value={password}
onChangeText={setPassword}
editable={!isLoading}
/>
<TextInput
style={styles.input}
placeholder="Confirm Password"
secureTextEntry
value={confirmPassword}
onChangeText={setConfirmPassword}
editable={!isLoading}
/>
<View style={styles.typeRow}>
<Pressable
style={[styles.typeButton, accountType === 'Individual' && styles.typeButtonActive]}
onPress={() => setAccountType('Individual')}
disabled={isLoading}
>
<Text style={[styles.typeText, accountType === 'Individual' && styles.typeTextActive]}>
Individual
</Text>
</Pressable>
<Pressable
style={[styles.typeButton, accountType === 'Business/School' && styles.typeButtonActive]}
onPress={() => setAccountType('Business/School')}
disabled={isLoading}
>
<Text style={[styles.typeText, accountType === 'Business/School' && styles.typeTextActive]}>
Business
</Text>
</Pressable>
</View>
{error ? <Text style={styles.errorText}>{error}</Text> : null}
<ButtonTray>
<Button
label={submitting ? 'Creating...' : 'Create Account'}
onClick={handleRegister}
styleButton={styles.submitButton}
styleLabel={styles.submitText}
/>
</ButtonTray>
<Pressable onPress={() => navigation.navigate('Auth', {screen: 'LoginScreen'})}>
<Text style={styles.linkText}>Back to Login</Text>
</Pressable>
</View>
</Screen>
);
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
justifyContent: 'center', justifyContent: 'center',
}, },
content: { content: {
gap: 12, gap: 12,
}, },
title: { title: {
fontSize: 24, fontSize: 24,
fontWeight: 'bold', fontWeight: 'bold',
textAlign: 'center', textAlign: 'center',
marginBottom: 10, marginBottom: 10,
}, },
input: { input: {
borderWidth: 1, borderWidth: 1,
borderColor: '#d1d5db', borderColor: '#d1d5db',
borderRadius: 8, borderRadius: 8,
paddingHorizontal: 12, paddingHorizontal: 12,
paddingVertical: 10, paddingVertical: 10,
fontSize: 16, fontSize: 16,
}, },
typeRow: { typeRow: {
flexDirection: 'row', flexDirection: 'row',
gap: 10, gap: 10,
}, },
typeButton: { typeButton: {
flex: 1, flex: 1,
borderWidth: 1, borderWidth: 1,
borderColor: '#cbd5e1', borderColor: '#cbd5e1',
borderRadius: 8, borderRadius: 8,
alignItems: 'center', alignItems: 'center',
paddingVertical: 10, paddingVertical: 10,
}, },
typeButtonActive: { typeButtonActive: {
backgroundColor: '#2563eb', backgroundColor: '#2563eb',
borderColor: '#2563eb', borderColor: '#2563eb',
}, },
typeText: { typeText: {
color: '#1f2937', color: '#1f2937',
fontWeight: '600', fontWeight: '600',
}, },
typeTextActive: { typeTextActive: {
color: '#ffffff', color: '#ffffff',
}, },
errorText: { errorText: {
color: '#dc2626', color: '#dc2626',
textAlign: 'center', textAlign: 'center',
marginTop: 5, marginTop: 5,
}, },
submitButton: { submitButton: {
backgroundColor: '#2563eb', backgroundColor: '#2563eb',
borderColor: '#2563eb', borderColor: '#2563eb',
}, },
submitText: { submitText: {
color: '#ffffff', color: '#ffffff',
fontWeight: '600', fontWeight: '600',
}, },
linkText: { linkText: {
textAlign: 'center', textAlign: 'center',
color: '#2563eb', color: '#2563eb',
fontWeight: '600', fontWeight: '600',
paddingVertical: 10, paddingVertical: 10,
}, },
}); });
export default RegisterScreen; export default RegisterScreen;
+16 -9
View File
@@ -2,17 +2,24 @@ import React from 'react';
import {StyleSheet, Text} from 'react-native'; import {StyleSheet, Text} from 'react-native';
import Screen from '../layout/Screen'; import Screen from '../layout/Screen';
const TeamScreen = () => ( const TeamScreen = () => {
<Screen style={styles.container}> // Initialisation -------------
<Text style={styles.title}>Team Management</Text> // State ----------------------
<Text style={styles.body}>Team management setup goes here.</Text> // Handlers -------------------
</Screen> // View -----------------------
);
return (
<Screen style={styles.container}>
<Text style={styles.title}>Team Management</Text>
<Text style={styles.body}>Team management setup goes here.</Text>
</Screen>
);
};
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { justifyContent: 'center', alignItems: 'center' }, container: {justifyContent: 'center', alignItems: 'center'},
title: { fontSize: 24, fontWeight: 'bold' }, title: {fontSize: 24, fontWeight: 'bold'},
body: { color: '#4b5563', fontSize: 15, marginTop: 10 }, body: {color: '#4b5563', fontSize: 15, marginTop: 10},
}); });
export default TeamScreen; export default TeamScreen;
+4
View File
@@ -1,3 +1,5 @@
// --- Session Store ---
const session = { const session = {
currentUid: null, currentUid: null,
currentGid: null, currentGid: null,
@@ -7,6 +9,8 @@ const session = {
isAcceptedAdmin: false, isAcceptedAdmin: false,
}; };
// --- Handlers ---
const setSessionUser = (user) => { const setSessionUser = (user) => {
session.currentUid = user?.Uid ?? null; session.currentUid = user?.Uid ?? null;
session.currentGid = user?.Gid ?? null; session.currentGid = user?.Gid ?? null;
+2 -21
View File
@@ -1,22 +1,3 @@
import Constants from 'expo-constants'; const API_BASE_URL = 'http://localhost:3000';
const hostFromExpo = Constants.expoConfig?.hostUri?.split(':')[0] || null; export default API_BASE_URL;
const apiBaseUrl = 'http://localhost:3000'; // Shirish, i need to deploy the database first so... wait until that... sorry buddy...
// if you want you can run your own db locally i can send you the files for it just ask :(((
const postJson = async (path, payload) => {
const response = await fetch(`${apiBaseUrl}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.message || 'Request failed');
}
return result;
};
export const postDb = async (action, payload = {}) => {
return postJson('/db', { action, payload });
};
+36 -25
View File
@@ -1,8 +1,16 @@
import {useState} from 'react'; import {useState} from 'react';
import {postDb} from './dbLink'; import API from '../components/API/API';
import API_BASE_URL from './dbLink';
import {clearSession, setSessionUser} from './SessionStore'; import {clearSession, setSessionUser} from './SessionStore';
const useAuthHook = () => { const useAuthHook = () => {
// Initialisation ------------
const loginEndpoint = `${API_BASE_URL}/auth/login`;
const registerEndpoint = `${API_BASE_URL}/auth/register`;
// State ----------------------
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
@@ -11,40 +19,41 @@ const useAuthHook = () => {
const [error, setError] = useState(''); const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
// Handlers -------------------
const login = async () => { const login = async () => {
setError(''); setError('');
setIsLoading(true); setIsLoading(true);
try { const response = await API.post(loginEndpoint, {
const user = await postDb('login', { email: email.trim(), password }); email: email.trim(),
setSessionUser(user); password,
setIsLoading(false); });
return user; setIsLoading(false);
} catch (e) { if (response.isSuccess) {
setError(e.message); setSessionUser(response.result);
setIsLoading(false); return response.result;
return null;
} }
setError(response.message);
return null;
}; };
const register = async () => { const register = async () => {
setError(''); setError('');
setIsLoading(true); setIsLoading(true);
try { const response = await API.post(registerEndpoint, {
const user = await postDb('register', { username: username.trim(),
username: username.trim(), email: email.trim().toLowerCase(),
email: email.trim().toLowerCase(), password,
password, confirmPassword,
confirmPassword, accountType,
accountType, });
}); setIsLoading(false);
setSessionUser(user); if (response.isSuccess) {
setIsLoading(false); setSessionUser(response.result);
return user; return response.result;
} catch (e) {
setError(e.message);
setIsLoading(false);
return null;
} }
setError(response.message);
return null;
}; };
const logout = () => { const logout = () => {
@@ -57,6 +66,8 @@ const useAuthHook = () => {
setError(''); setError('');
}; };
// Return ---------------------
return { return {
email, email,
password, password,
+84 -15
View File
@@ -1,33 +1,102 @@
import {postDb} from './dbLink'; import API from '../components/API/API';
import API_BASE_URL from './dbLink';
const useGameHook = () => { const useGameHook = () => {
const request = async (action, payload = {}) => postDb(action, payload); // Initialisation ------------
const getGameTypes = async () => request('getGameTypes'); const gameTypesEndpoint = `${API_BASE_URL}/game-types`;
const groupsEndpoint = `${API_BASE_URL}/groups`;
const subgroupMembershipsEndpoint = `${API_BASE_URL}/subgroup-memberships`;
const teamsEndpoint = `${API_BASE_URL}/teams`;
const teamMembersEndpoint = `${API_BASE_URL}/team-members`;
const gameDataEndpoint = `${API_BASE_URL}/game-data`;
const adminWaitlistEndpoint = `${API_BASE_URL}/admin-waitlist`;
const getCreatedPrivateGame = async (userId) => request('getCreatedPrivateGameByUser', { userId }); // Handlers -------------------
const createPrivateGame = async (payload) => request('createPrivateGame', payload); const getGameTypes = async () => {
const response = await API.get(gameTypesEndpoint);
return response.isSuccess ? response.result : [];
};
const joinPrivateGame = async (payload) => request('joinPrivateGame', payload); const getCreatedPrivateGame = async (userId) => {
const response = await API.get(`${groupsEndpoint}?CreatedByUid=${userId}`);
return response.isSuccess ? response.result : [];
};
const joinAsAdmin = async (payload) => request('joinAsAdmin', payload); const createPrivateGame = async (payload) => {
const response = await API.post(groupsEndpoint, payload);
return response.isSuccess ? response.result : null;
};
const getLobby = async (gid) => request('getLobby', { gid }); const joinPrivateGame = async (payload) => {
const response = await API.post(subgroupMembershipsEndpoint, payload);
return response.isSuccess ? response.result : null;
};
const getTeam = async (tid) => request('getTeam', { tid }); const joinAsAdmin = async (payload) => {
const response = await API.post(adminWaitlistEndpoint, payload);
return response.isSuccess ? response.result : null;
};
const createTeam = async (payload) => request('createTeam', payload); const getLobby = async (gid) => {
const response = await API.get(`${groupsEndpoint}/${gid}`);
return response.isSuccess ? response.result : null;
};
const joinTeamByCode = async (payload) => request('joinTeamByCode', payload); const getTeam = async (tid) => {
const response = await API.get(`${teamsEndpoint}/${tid}`);
return response.isSuccess ? response.result : null;
};
const getMapPoints = async (gid, sgid = null) => request('getMapPoints', { gid, sgid }); const createTeam = async (payload) => {
const response = await API.post(teamsEndpoint, payload);
return response.isSuccess ? response.result : null;
};
const getCaches = async (gid, sgid = null) => request('getCaches', { gid, sgid }); const joinTeamByCode = async (payload) => {
const response = await API.post(teamMembersEndpoint, payload);
return response.isSuccess ? response.result : null;
};
const upsertCache = async (payload) => request('upsertCache', payload); const getMapPoints = async (gid, sgid = null) => {
let url = `${gameDataEndpoint}/${gid}/caches`;
if (sgid !== null) url += `?SGid=${sgid}`;
const response = await API.get(url);
return response.isSuccess ? response.result : [];
};
const claimCache = async (payload) => request('claimCache', payload); const getCaches = async (gid, sgid = null) => {
let url = `${gameDataEndpoint}/${gid}/caches`;
if (sgid !== null) url += `?SGid=${sgid}`;
const response = await API.get(url);
return response.isSuccess ? response.result : [];
};
const upsertCache = async (payload) => {
if (payload.cacheId) {
const response = await API.put(
`${gameDataEndpoint}/${payload.gid}/caches/${payload.cacheId}`,
payload,
);
return response.isSuccess ? response.result : null;
}
const response = await API.post(
`${gameDataEndpoint}/${payload.gid}/caches`,
payload,
);
return response.isSuccess ? response.result : null;
};
const claimCache = async (payload) => {
const response = await API.put(
`${gameDataEndpoint}/${payload.gid}/caches/${payload.cacheId}`,
{ClaimedByUid: payload.uid, ClaimedByTid: payload.tid},
);
return response.isSuccess ? response.result : null;
};
// Return ---------------------
return { return {
getGameTypes, getGameTypes,
+25 -24
View File
@@ -2,36 +2,37 @@ import {useEffect, useState} from 'react';
import {isLookingAtCache, isWithinRadius} from '../utils/geoMath'; import {isLookingAtCache, isWithinRadius} from '../utils/geoMath';
const usePlayerGame = (playerLocation, playerHeading, activeCaches) => { const usePlayerGame = (playerLocation, playerHeading, activeCaches) => {
const [visibleCache, setVisibleCache] = useState(null); // State ----------------------
const [isClaiming, setIsClaiming] = useState(false);
useEffect(() => { const [visibleCache, setVisibleCache] = useState(null);
if (!playerLocation || playerHeading === null || playerHeading === undefined) { const [isClaiming, setIsClaiming] = useState(false);
setVisibleCache(null);
setIsClaiming(false);
return;
}
const nearbyCache = (activeCaches || []).find((cache) => // Handlers -------------------
isWithinRadius(playerLocation, cache.coordinates, cache.radius));
if (!nearbyCache) { useEffect(() => {
setVisibleCache(null); if (!playerLocation || playerHeading === null || playerHeading === undefined) {
setIsClaiming(false); setVisibleCache(null);
return; setIsClaiming(false);
} return;
}
const isTargeted = isLookingAtCache( const nearbyCache = (activeCaches || []).find((cache) =>
playerHeading, isWithinRadius(playerLocation, cache.coordinates, cache.radius));
playerLocation,
nearbyCache.coordinates,
);
setVisibleCache(nearbyCache); if (!nearbyCache) {
setIsClaiming(isTargeted); setVisibleCache(null);
}, [playerLocation, playerHeading, activeCaches]); setIsClaiming(false);
return;
}
return { visibleCache, isClaiming, setIsClaiming }; const isTargeted = isLookingAtCache(playerHeading, playerLocation, nearbyCache.coordinates);
setVisibleCache(nearbyCache);
setIsClaiming(isTargeted);
}, [playerLocation, playerHeading, activeCaches]);
// Return ---------------------
return {visibleCache, isClaiming, setIsClaiming};
}; };
export default usePlayerGame; export default usePlayerGame;