Add GameSettings and Players screens; implement cache management features in MapScreen

This commit is contained in:
bobbert committed 2026-04-06 23:59:52 +01:00
1 parent b8efe987af
commit 8008a9b6fe
22 files changed
+906 -81

No files matched your search

+8 -2
View File
@@ -14,6 +14,8 @@ import MapScreen from "./src/components/screens/MapScreen";
import LeaderboardScreen from "./src/components/screens/LeaderboardScreen"; import LeaderboardScreen from "./src/components/screens/LeaderboardScreen";
import AdminHubScreen from "./src/components/screens/AdminHubScreen"; import AdminHubScreen from "./src/components/screens/AdminHubScreen";
import ManageSubgroupsScreen from "./src/components/screens/ManageSubgroupsScreen"; import ManageSubgroupsScreen from "./src/components/screens/ManageSubgroupsScreen";
import GameSettingsScreen from "./src/components/screens/GameSettingsScreen";
import PlayersScreen from "./src/components/screens/PlayersScreen";
const RootStack = createNativeStackNavigator(); const RootStack = createNativeStackNavigator();
const AuthStackNav = createNativeStackNavigator(); const AuthStackNav = createNativeStackNavigator();
@@ -43,6 +45,8 @@ const GameStack = () => (
<GameStackNav.Screen name="MapScreen" component={MapScreen}/> <GameStackNav.Screen name="MapScreen" component={MapScreen}/>
<GameStackNav.Screen name="TeamScreen" component={TeamScreen}/> <GameStackNav.Screen name="TeamScreen" component={TeamScreen}/>
<GameStackNav.Screen name="LeaderboardScreen" component={LeaderboardScreen}/> <GameStackNav.Screen name="LeaderboardScreen" component={LeaderboardScreen}/>
<GameStackNav.Screen name="GameSettingsScreen" component={GameSettingsScreen}/>
<GameStackNav.Screen name="PlayersScreen" component={PlayersScreen}/>
<GameStackNav.Screen name="GameTypeScreen" component={GameTypeScreen}/> <GameStackNav.Screen name="GameTypeScreen" component={GameTypeScreen}/>
<GameStackNav.Screen name="JoinPrivateGameScreen" component={JoinPrivateGameScreen}/> <GameStackNav.Screen name="JoinPrivateGameScreen" component={JoinPrivateGameScreen}/>
<GameStackNav.Screen name="ManagePrivateGameScreen" component={ManagePrivateGameScreen}/> <GameStackNav.Screen name="ManagePrivateGameScreen" component={ManagePrivateGameScreen}/>
@@ -59,10 +63,12 @@ const AdminStack = () => (
contentStyle: { backgroundColor: '#ffffff' }, contentStyle: { backgroundColor: '#ffffff' },
}} }}
> >
<AdminStackNav.Screen 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="AdminHubScreen" component={AdminHubScreen}/> <AdminStackNav.Screen name="AdminHubScreen" component={AdminHubScreen}/>
<AdminStackNav.Screen name="ManageSubgroupsScreen" component={ManageSubgroupsScreen}/> <AdminStackNav.Screen name="ManageSubgroupsScreen" component={ManageSubgroupsScreen}/>
<AdminStackNav.Screen name="LeaderboardScreen" component={LeaderboardScreen}/>
<AdminStackNav.Screen name="MapScreen" component={MapScreen}/>
<AdminStackNav.Screen name="TeamScreen" component={TeamScreen}/> <AdminStackNav.Screen name="TeamScreen" component={TeamScreen}/>
<AdminStackNav.Screen name="CreateGameScreen" component={CreateGameScreen}/> <AdminStackNav.Screen name="CreateGameScreen" component={CreateGameScreen}/>
<AdminStackNav.Screen name="ManageGameScreen" component={ManageGameScreen}/> <AdminStackNav.Screen name="ManageGameScreen" component={ManageGameScreen}/>
+16 -2
View File
@@ -13,14 +13,28 @@
"backgroundColor": "#ffffff" "backgroundColor": "#ffffff"
}, },
"ios": { "ios": {
"supportsTablet": true "supportsTablet": true,
"infoPlist": {
"NSLocationWhenInUseUsageDescription": "This app needs your location for the treasure hunt."
}
}, },
"android": { "android": {
"adaptiveIcon": { "adaptiveIcon": {
"foregroundImage": "./assets/images/android-icon-monochrome.png", "foregroundImage": "./assets/images/android-icon-monochrome.png",
"backgroundColor": "#ffffff" "backgroundColor": "#ffffff"
}, },
"edgeToEdgeEnabled": false "config": {
"googleMaps": {
"apiKey": "AIzaSyA-G0tSzzcM5z6HyI23s8dzKDLCOrekhbw"
}
},
"permissions": [
"ACCESS_FINE_LOCATION",
"ACCESS_COARSE_LOCATION",
"ACCESS_FINE_LOCATION",
"ACCESS_COARSE_LOCATION"
],
"package": "com.bobthebob.MAD_Treasure_Hunt_App"
}, },
"web": { "web": {
"favicon": "./assets/favicon.png" "favicon": "./assets/favicon.png"
+2 -2
View File
@@ -1,5 +1,5 @@
export const API_BASE_URL = 'http://api.bobby.ip-ddns.com'; //For outside Home network //export const API_BASE_URL = 'http://api.bobby.ip-ddns.com'; //For outside Home network
//export const API_BASE_URL = 'http://192.168.8.100:3000'; //For when im home (API Endpoint is on my home server...) export const API_BASE_URL = 'http://192.168.8.100:3000'; //For when im home (API Endpoint is on my home server...)
const API = {}; const API = {};
API.get = (endpoint) => callFetch(endpoint, 'GET'); API.get = (endpoint) => callFetch(endpoint, 'GET');
+26
View File
@@ -0,0 +1,26 @@
import {StyleSheet, View} from 'react-native';
const Card = ({children, style}) => {
// Initialisation -------------
// State ----------------------
// Handlers -------------------
// View -----------------------
return (
<View style={[styles.card, style]}>{children}</View>
);
};
const styles = StyleSheet.create({
card: {
backgroundColor: '#ffffff',
borderRadius: 8,
padding: 12,
marginBottom: 8,
borderWidth: 1,
borderColor: '#e5e7eb',
},
});
export default Card;
@@ -19,12 +19,9 @@ const AdminCacheEditorView = ({ userLocation, caches, onAddCache, onMoveCache })
latitudeDelta: 0.01, latitudeDelta: 0.01,
longitudeDelta: 0.01, longitudeDelta: 0.01,
}} }}
region={{
...userLocation,
latitudeDelta: 0.01,
longitudeDelta: 0.01,
}}
onLongPress={(event) => onAddCache(event.nativeEvent.coordinate)} onLongPress={(event) => onAddCache(event.nativeEvent.coordinate)}
scrollEnabled={true}
zoomEnabled={true}
showsUserLocation showsUserLocation
> >
{(caches || []).map((cache) => ( {(caches || []).map((cache) => (
+86
View File
@@ -0,0 +1,86 @@
import {StyleSheet, Text, View} from 'react-native';
import Card from '../UI/Card';
import {Button} from '../UI/Button';
const CacheCardItem = ({cache, isAdmin, onEdit, onDelete, onSelect}) => {
// Initialisation -------------
const isClaimed = Boolean(cache.ClaimedByUid);
const statusText = isClaimed ? 'Claimed' : 'Available';
const statusColor = isClaimed ? '#9ca3af' : '#16a34a';
// State ----------------------
// Handlers -------------------
// View -----------------------
return (
<Card>
<View style={styles.topRow}>
<Text style={styles.clue} numberOfLines={1}>{cache.clue}</Text>
<Text style={[styles.status, {color: statusColor}]}>{statusText}</Text>
</View>
<View style={styles.actions}>
{isAdmin ? (
<>
<Button
label="Edit"
onClick={() => onEdit(cache)}
styleButton={styles.editBtn}
styleLabel={styles.btnLabel}
/>
<Button
label="Delete"
onClick={() => onDelete(cache)}
styleButton={styles.deleteBtn}
styleLabel={styles.btnLabel}
/>
</>
) : (
<Button
label="Select Cache"
onClick={() => onSelect(cache)}
styleButton={styles.selectBtn}
styleLabel={styles.btnLabel}
/>
)}
</View>
</Card>
);
};
const styles = StyleSheet.create({
topRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8,
},
clue: {fontSize: 15, fontWeight: '600', color: '#1f2937', flex: 1, marginRight: 8},
status: {fontSize: 13, fontWeight: '600'},
actions: {flexDirection: 'row', gap: 8},
editBtn: {
backgroundColor: '#2563eb',
borderColor: '#2563eb',
minHeight: 36,
paddingHorizontal: 14,
flex: 1,
},
deleteBtn: {
backgroundColor: '#dc2626',
borderColor: '#dc2626',
minHeight: 36,
paddingHorizontal: 14,
flex: 1,
},
selectBtn: {
backgroundColor: '#16a34a',
borderColor: '#16a34a',
minHeight: 36,
paddingHorizontal: 14,
flex: 1,
},
btnLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13},
});
export default CacheCardItem;
-5
View File
@@ -19,11 +19,6 @@ const PlayerMapView = ({ userLocation, visibleCache, isClaiming, onClaimSuccess
latitudeDelta: 0.01, latitudeDelta: 0.01,
longitudeDelta: 0.01, longitudeDelta: 0.01,
}} }}
region={{
...userLocation,
latitudeDelta: 0.01,
longitudeDelta: 0.01,
}}
showsUserLocation showsUserLocation
> >
{visibleCache ? ( {visibleCache ? (
+31 -4
View File
@@ -1,29 +1,56 @@
import {Pressable, StyleSheet, Text, View} from 'react-native'; import {Pressable, StyleSheet, Text, View} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context'; import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {getSession} from '../../hooks/SessionStore';
const BottomNavbar = ({ navigation, routeName }) => { const BottomNavbar = ({ navigation, routeName }) => {
// Initialisation ------------ // Initialisation ------------
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const session = getSession();
const tabs = [ const adminTabs = [
{
label: 'Game Settings',
route: 'GameSettingsScreen',
onPress: () => navigation.navigate('GameSettingsScreen'),
},
{ {
label: 'Map', label: 'Map',
route: 'MapScreen', route: 'MapScreen',
onPress: () => navigation.navigate('Game', { screen: 'MapScreen' }), onPress: () => navigation.navigate('MapScreen'),
},
{
label: 'Players',
route: 'PlayersScreen',
onPress: () => navigation.navigate('PlayersScreen'),
}, },
{ {
label: 'Leaderboard', label: 'Leaderboard',
route: 'LeaderboardScreen', route: 'LeaderboardScreen',
onPress: () => navigation.navigate('Game', { screen: 'LeaderboardScreen' }), onPress: () => navigation.navigate('LeaderboardScreen'),
},
];
const playerTabs = [
{
label: 'Map',
route: 'MapScreen',
onPress: () => navigation.navigate('MapScreen'),
},
{
label: 'Leaderboard',
route: 'LeaderboardScreen',
onPress: () => navigation.navigate('LeaderboardScreen'),
}, },
{ {
label: 'Current Team', label: 'Current Team',
route: 'TeamScreen', route: 'TeamScreen',
onPress: () => navigation.navigate('Game', { screen: 'TeamScreen' }), onPress: () => navigation.navigate('TeamScreen'),
}, },
]; ];
const tabs = session.isAcceptedAdmin ? adminTabs : playerTabs;
// State ---------------------- // State ----------------------
// Handlers ------------------- // Handlers -------------------
// View ----------------------- // View -----------------------
+3 -2
View File
@@ -5,7 +5,7 @@ import {useSafeAreaInsets} from "react-native-safe-area-context";
import TopNavbar from "./TopNavbar"; import TopNavbar from "./TopNavbar";
import BottomNavbar from "./BottomNavbar"; import BottomNavbar from "./BottomNavbar";
const Screen = ({ children, style }) => { const Screen = ({ children, style, showBack = false }) => {
// Initialisation ------------- // Initialisation -------------
const navigation = useNavigation(); const navigation = useNavigation();
@@ -24,13 +24,14 @@ const Screen = ({ children, style }) => {
MapScreen: "Map", MapScreen: "Map",
AdminHubScreen: "Admin Hub", AdminHubScreen: "Admin Hub",
ManageSubgroupsScreen: "Manage Subgroups", ManageSubgroupsScreen: "Manage Subgroups",
GameSettingsScreen: "Game Settings",
PlayersScreen: "Players",
}; };
// State ---------------------- // State ----------------------
const routeName = route.name; const routeName = route.name;
const isAuthScreen = routeName === "LoginScreen" || routeName === "RegisterScreen"; const isAuthScreen = routeName === "LoginScreen" || routeName === "RegisterScreen";
const showBack = !isAuthScreen && navigation.canGoBack();
const pageTitle = titleMap[routeName] || "Treasure Hunt"; const pageTitle = titleMap[routeName] || "Treasure Hunt";
// Handlers ------------------- // Handlers -------------------
+1 -1
View File
@@ -9,7 +9,7 @@ const AdminHubScreen = () => {
// View ----------------------- // View -----------------------
return ( return (
<Screen style={styles.container}> <Screen showBack={true} style={styles.container}>
<Text style={styles.title}>Admin Hub</Text> <Text style={styles.title}>Admin Hub</Text>
<Text style={styles.body}>Admin hub setup goes here.</Text> <Text style={styles.body}>Admin hub setup goes here.</Text>
</Screen> </Screen>
+1 -1
View File
@@ -9,7 +9,7 @@ const CreateGameScreen = () => {
// View ----------------------- // View -----------------------
return ( return (
<Screen style={styles.container}> <Screen showBack={true} style={styles.container}>
<Text style={styles.title}>Create Game</Text> <Text style={styles.title}>Create Game</Text>
<Text style={styles.body}>Create game setup goes here.</Text> <Text style={styles.body}>Create game setup goes here.</Text>
</Screen> </Screen>
+1 -1
View File
@@ -9,7 +9,7 @@ const GameLobbyScreen = () => {
// View ----------------------- // View -----------------------
return ( return (
<Screen style={styles.container}> <Screen showBack={true} style={styles.container}>
<Text style={styles.title}>Game Lobby</Text> <Text style={styles.title}>Game Lobby</Text>
<Text style={styles.body}>Game lobby setup goes here.</Text> <Text style={styles.body}>Game lobby setup goes here.</Text>
</Screen> </Screen>
@@ -0,0 +1,174 @@
import React, {useEffect, useState} from 'react';
import {ActivityIndicator, StyleSheet, Switch, Text, TextInput, View} from 'react-native';
import Screen from '../layout/Screen';
import {Button, ButtonTray} from '../UI/Button';
import useGameHook from '../../hooks/useGameHook';
import {getSession} from '../../hooks/SessionStore';
const GameSettingsScreen = () => {
// Initialisation ------------
const session = getSession();
const {getLobby, updateGroup, getSubgroups} = useGameHook();
const isBusiness = Boolean(session.isBusiness);
// State ----------------------
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [groupName, setGroupName] = useState('');
const [teamsEnabled, setTeamsEnabled] = useState(false);
const [maxSubgroups, setMaxSubgroups] = useState('1');
const [adminJoinCode, setAdminJoinCode] = useState('');
const [memberJoinCode, setMemberJoinCode] = useState('');
// Handlers -------------------
useEffect(() => {
const load = async () => {
if (!session.currentGid) {
setLoading(false);
return;
}
const group = await getLobby(session.currentGid);
if (group) {
setGroupName(group.GroupName || '');
setTeamsEnabled(Boolean(group.TeamsEnabled));
setMaxSubgroups(String(group.MaxMemberSubgroups || 1));
setAdminJoinCode(group.AdminJoinCode || '');
}
const sgs = await getSubgroups(session.currentGid);
const memberSg = (sgs || []).find((sg) => !sg.IsAdminGroup);
if (memberSg) setMemberJoinCode(memberSg.JoinCode || '');
setLoading(false);
};
load();
}, []);
const handleSave = async () => {
if (!session.currentGid) return;
setSaving(true);
await updateGroup(session.currentGid, {
GroupName: groupName.trim(),
TeamsEnabled: teamsEnabled,
MaxMemberSubgroups: parseInt(maxSubgroups) || 1,
});
setSaving(false);
};
// View -----------------------
if (loading) {
return (
<Screen style={styles.center}>
<ActivityIndicator size="large"/>
</Screen>
);
}
if (!session.currentGid) {
return (
<Screen style={styles.center}>
<Text style={styles.body}>Create or join a game first.</Text>
</Screen>
);
}
return (
<Screen>
<Text style={styles.sectionTitle}>Game Configuration</Text>
<Text style={styles.label}>Game Name</Text>
<TextInput
style={styles.input}
value={groupName}
onChangeText={setGroupName}
placeholder="Enter game name"
/>
<View style={styles.toggleRow}>
<Text style={styles.label}>Teams Enabled</Text>
<Switch
value={teamsEnabled}
onValueChange={setTeamsEnabled}
trackColor={{false: '#d1d5db', true: '#93c5fd'}}
thumbColor={teamsEnabled ? '#2563eb' : '#9ca3af'}
/>
</View>
{isBusiness && (
<>
<Text style={styles.label}>Max Member Subgroups</Text>
<TextInput
style={styles.input}
value={maxSubgroups}
onChangeText={setMaxSubgroups}
keyboardType="numeric"
placeholder="1"
/>
</>
)}
<View style={styles.codeSection}>
<Text style={styles.codeLabel}>Admin Join Code</Text>
<Text style={styles.codeValue}>{adminJoinCode || '—'}</Text>
</View>
<View style={styles.codeSection}>
<Text style={styles.codeLabel}>Player Join Code</Text>
<Text style={styles.codeValue}>{memberJoinCode || '—'}</Text>
</View>
<ButtonTray>
<Button
label={saving ? 'Saving...' : 'Save Settings'}
onClick={handleSave}
styleButton={styles.saveButton}
styleLabel={styles.saveLabel}
/>
</ButtonTray>
</Screen>
);
};
const styles = StyleSheet.create({
center: {justifyContent: 'center', alignItems: 'center'},
sectionTitle: {fontSize: 20, fontWeight: '700', color: '#1f2937', marginBottom: 15},
label: {fontSize: 14, fontWeight: '600', color: '#374151', marginBottom: 4, marginTop: 10},
body: {color: '#4b5563', fontSize: 15},
input: {
borderWidth: 1,
borderColor: '#d1d5db',
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 10,
fontSize: 16,
backgroundColor: '#ffffff',
},
toggleRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: 10,
paddingVertical: 8,
borderBottomWidth: 1,
borderBottomColor: '#e5e7eb',
},
codeSection: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: 14,
paddingVertical: 10,
paddingHorizontal: 12,
backgroundColor: '#f3f4f6',
borderRadius: 8,
},
codeLabel: {fontSize: 14, fontWeight: '600', color: '#374151'},
codeValue: {fontSize: 16, fontWeight: '700', color: '#2563eb', letterSpacing: 2},
saveButton: {backgroundColor: '#16a34a', borderColor: '#16a34a'},
saveLabel: {color: '#ffffff', fontWeight: '600'},
});
export default GameSettingsScreen;
+1 -1
View File
@@ -27,7 +27,7 @@ const GameTypeScreen = ({navigation}) => {
// View ----------------------- // View -----------------------
return ( return (
<Screen style={styles.container}> <Screen showBack={true} style={styles.container}>
<Text style={styles.title}>Select Game Type</Text> <Text style={styles.title}>Select Game Type</Text>
<View style={styles.content}> <View style={styles.content}>
{gameTypes && gameTypes.map((type) => ( {gameTypes && gameTypes.map((type) => (
@@ -9,7 +9,7 @@ const JoinPrivateGameScreen = () => {
// View ----------------------- // View -----------------------
return ( return (
<Screen style={styles.container}> <Screen showBack={true} style={styles.container}>
<Text style={styles.title}>Join Private Game</Text> <Text style={styles.title}>Join Private Game</Text>
<Text style={styles.body}>Join private game setup goes here.</Text> <Text style={styles.body}>Join private game setup goes here.</Text>
</Screen> </Screen>
+5 -5
View File
@@ -89,7 +89,7 @@ const ManageGameScreen = () => {
if (!session.currentGid) { if (!session.currentGid) {
return ( return (
<Screen style={styles.center}> <Screen showBack={true} style={styles.center}>
<Text style={styles.body}>Create or join a game first.</Text> <Text style={styles.body}>Create or join a game first.</Text>
</Screen> </Screen>
); );
@@ -97,7 +97,7 @@ const ManageGameScreen = () => {
if (!session.isAcceptedAdmin) { if (!session.isAcceptedAdmin) {
return ( return (
<Screen style={styles.center}> <Screen showBack={true} style={styles.center}>
<Text style={styles.body}>Only admins can edit cache locations.</Text> <Text style={styles.body}>Only admins can edit cache locations.</Text>
</Screen> </Screen>
); );
@@ -105,7 +105,7 @@ const ManageGameScreen = () => {
if (loading) { if (loading) {
return ( return (
<Screen style={styles.center}> <Screen showBack={true} style={styles.center}>
<ActivityIndicator size="large"/> <ActivityIndicator size="large"/>
</Screen> </Screen>
); );
@@ -113,14 +113,14 @@ const ManageGameScreen = () => {
if (error) { if (error) {
return ( return (
<Screen style={styles.center}> <Screen showBack={true} style={styles.center}>
<Text style={styles.error}>{error}</Text> <Text style={styles.error}>{error}</Text>
</Screen> </Screen>
); );
} }
return ( return (
<Screen style={styles.container}> <Screen showBack={true} style={styles.container}>
<View style={styles.mapWrap}> <View style={styles.mapWrap}>
<AdminCacheEditorView <AdminCacheEditorView
userLocation={userLocation} userLocation={userLocation}
@@ -9,7 +9,7 @@ const ManagePrivateGameScreen = () => {
// View ----------------------- // View -----------------------
return ( return (
<Screen style={styles.container}> <Screen showBack={true} style={styles.container}>
<Text style={styles.title}>Manage Private Game</Text> <Text style={styles.title}>Manage Private Game</Text>
<Text style={styles.body}>Manage private game setup goes here.</Text> <Text style={styles.body}>Manage private game setup goes here.</Text>
</Screen> </Screen>
@@ -9,7 +9,7 @@ const ManageSubgroupsScreen = () => {
// View ----------------------- // View -----------------------
return ( return (
<Screen style={styles.container}> <Screen showBack={true} style={styles.container}>
<Text style={styles.title}>Manage Subgroups</Text> <Text style={styles.title}>Manage Subgroups</Text>
<Text style={styles.body}>Manage subgroups setup goes here.</Text> <Text style={styles.body}>Manage subgroups setup goes here.</Text>
</Screen> </Screen>
+263 -45
View File
@@ -1,14 +1,18 @@
import React, {useCallback, useEffect, useState} from 'react'; import React, {useCallback, useEffect, useState} from 'react';
import {ActivityIndicator, ScrollView, StyleSheet, Text, TextInput, View} from 'react-native'; import {ActivityIndicator, ScrollView, StyleSheet, Text, TextInput, View} from 'react-native';
import MapView, {Circle, Marker} from 'react-native-maps';
import * as Location from 'expo-location'; import * as Location from 'expo-location';
import {Magnetometer} from 'expo-sensors'; import {Magnetometer} from 'expo-sensors';
import Screen from '../layout/Screen'; import Screen from '../layout/Screen';
import {Button, ButtonTray} from '../UI/Button'; import {Button, ButtonTray} from '../UI/Button';
import CacheCardItem from '../gameplay/CacheCardItem';
import PlayerMapView from '../gameplay/PlayerMapView'; import PlayerMapView from '../gameplay/PlayerMapView';
import useGameHook from '../../hooks/useGameHook'; import useGameHook from '../../hooks/useGameHook';
import usePlayerGame from '../../hooks/usePlayerGame'; import usePlayerGame from '../../hooks/usePlayerGame';
import {getSession, setSessionGroup, setSessionUser} from '../../hooks/SessionStore'; import {getSession, setSessionGroup, setSessionUser} from '../../hooks/SessionStore';
const DEFAULT_REGION = {latitude: 51.5074, longitude: -0.1278, latitudeDelta: 0.05, longitudeDelta: 0.05};
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;
@@ -18,7 +22,7 @@ const MapScreen = ({navigation}) => {
// Initialisation ------------ // Initialisation ------------
const session = getSession(); const session = getSession();
const {getCaches, claimCache, joinPrivateGame, createPrivateGame, getLobby, getUser} = useGameHook(); const {getCaches, claimCache, upsertCache, deleteCache, joinPrivateGame, createPrivateGame, getLobby, getUser} = useGameHook();
// State ---------------------- // State ----------------------
@@ -26,12 +30,19 @@ const MapScreen = ({navigation}) => {
const [isAdmin, setIsAdmin] = useState(session.isAcceptedAdmin); const [isAdmin, setIsAdmin] = useState(session.isAcceptedAdmin);
const [userLocation, setUserLocation] = useState(null); const [userLocation, setUserLocation] = useState(null);
const [heading, setHeading] = useState(null); const [heading, setHeading] = useState(null);
const [loading, setLoading] = useState(true); const [playerLoading, setPlayerLoading] = useState(true);
const [cacheRecords, setCacheRecords] = useState([]); const [cacheRecords, setCacheRecords] = useState([]);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [gameCode, setGameCode] = useState(''); const [gameCode, setGameCode] = useState('');
const [groupInfo, setGroupInfo] = useState(null); const [groupInfo, setGroupInfo] = useState(null);
// Admin cache creation / editing state
const [isCreating, setIsCreating] = useState(false);
const [editingCacheId, setEditingCacheId] = useState(null);
const [newCoord, setNewCoord] = useState(null);
const [newClue, setNewClue] = useState('');
const [newRadius, setNewRadius] = useState('20');
const isPlayer = inGame && !isAdmin; const isPlayer = inGame && !isAdmin;
const {visibleCache, isClaiming, setIsClaiming} = usePlayerGame( const {visibleCache, isClaiming, setIsClaiming} = usePlayerGame(
isPlayer ? userLocation : null, isPlayer ? userLocation : null,
@@ -39,32 +50,32 @@ const MapScreen = ({navigation}) => {
isPlayer ? cacheRecords : [], isPlayer ? cacheRecords : [],
); );
const mapRegion = userLocation
? {...userLocation, latitudeDelta: 0.01, longitudeDelta: 0.01}
: DEFAULT_REGION;
// Handlers ------------------- // Handlers -------------------
// Load group info for TeamsEnabled check
useEffect(() => { useEffect(() => {
if (!session.currentGid) return; if (!session.currentGid) return;
getLobby(session.currentGid).then(setGroupInfo); getLobby(session.currentGid).then(setGroupInfo);
}, [inGame]); }, [inGame]);
// Load caches for players only
const loadCaches = useCallback(async () => { const loadCaches = useCallback(async () => {
if (!session.currentGid || isAdmin) { if (!session.currentGid) {
setCacheRecords([]); setCacheRecords([]);
return; return;
} }
const rows = await getCaches(session.currentGid, session.currentSGid); const sgFilter = isAdmin ? null : session.currentSGid;
const rows = await getCaches(session.currentGid, sgFilter);
setCacheRecords(rows || []); setCacheRecords(rows || []);
}, [inGame, isAdmin]); }, [inGame, isAdmin]);
useEffect(() => { loadCaches(); }, [loadCaches]); useEffect(() => { loadCaches(); }, [loadCaches]);
// Location + compass tracking for players only // Location tracking
useEffect(() => { useEffect(() => {
if (!isPlayer) { if (!inGame) return;
setLoading(false);
return;
}
let locationSub; let locationSub;
let headingSub; let headingSub;
@@ -72,7 +83,7 @@ const MapScreen = ({navigation}) => {
const {status} = await Location.requestForegroundPermissionsAsync(); const {status} = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') { if (status !== 'granted') {
setError('Location permission denied'); setError('Location permission denied');
setLoading(false); setPlayerLoading(false);
return; return;
} }
const current = await Location.getCurrentPositionAsync({accuracy: Location.Accuracy.Balanced}); const current = await Location.getCurrentPositionAsync({accuracy: Location.Accuracy.Balanced});
@@ -83,9 +94,11 @@ const MapScreen = ({navigation}) => {
(next) => setUserLocation({latitude: next.coords.latitude, longitude: next.coords.longitude}), (next) => setUserLocation({latitude: next.coords.latitude, longitude: next.coords.longitude}),
); );
if (isPlayer) {
Magnetometer.setUpdateInterval(500); Magnetometer.setUpdateInterval(500);
headingSub = Magnetometer.addListener((data) => setHeading(toHeading(data))); headingSub = Magnetometer.addListener((data) => setHeading(toHeading(data)));
setLoading(false); }
setPlayerLoading(false);
}; };
start(); start();
@@ -93,7 +106,7 @@ const MapScreen = ({navigation}) => {
if (locationSub) locationSub.remove(); if (locationSub) locationSub.remove();
if (headingSub) headingSub.remove(); if (headingSub) headingSub.remove();
}; };
}, [isPlayer]); }, [inGame, isPlayer]);
const handleJoinGame = async () => { const handleJoinGame = async () => {
if (!gameCode.trim()) return; if (!gameCode.trim()) return;
@@ -118,6 +131,7 @@ const MapScreen = ({navigation}) => {
setSessionUser(freshUser); setSessionUser(freshUser);
setInGame(true); setInGame(true);
setIsAdmin(true); setIsAdmin(true);
navigation.navigate('GameSettingsScreen');
} }
}; };
@@ -133,13 +147,67 @@ const MapScreen = ({navigation}) => {
await loadCaches(); await loadCaches();
}; };
// Admin — open create form
const handleCreateCachePress = () => {
const fallback = userLocation || {latitude: mapRegion.latitude, longitude: mapRegion.longitude};
setEditingCacheId(null);
setNewCoord(fallback);
setNewClue('');
setNewRadius('20');
setIsCreating(true);
};
// Admin — open edit form
const handleEditCache = (cache) => {
setEditingCacheId(cache.id);
setNewCoord(cache.coordinates);
setNewClue(cache.clue || '');
setNewRadius(String(cache.radius || 20));
setIsCreating(true);
};
// Admin — delete cache
const handleDeleteCache = async (cache) => {
if (!session.currentGid) return;
await deleteCache(session.currentGid, cache.id);
await loadCaches();
};
// Admin — save create or edit
const handleSaveCache = async () => {
if (!newCoord || !newClue.trim() || !session.currentGid) return;
const payload = {
gid: session.currentGid,
latitude: newCoord.latitude,
longitude: newCoord.longitude,
radius: parseInt(newRadius) || 20,
clue: newClue.trim(),
subgroupId: 1,
};
if (editingCacheId) payload.cacheId = editingCacheId;
await upsertCache(payload);
setIsCreating(false);
setEditingCacheId(null);
await loadCaches();
};
const handleCancelCreate = () => {
setIsCreating(false);
setEditingCacheId(null);
};
// Player — select cache (scroll to / highlight)
const handleSelectCache = (cache) => {
// Future: scroll map to cache location
};
// View ----------------------- // View -----------------------
// Not in a game // Not in a game
if (!inGame) { if (!inGame) {
return ( return (
<Screen style={styles.center}> <Screen style={styles.center}>
<View style={styles.row}> <View style={styles.inputRow}>
<TextInput <TextInput
style={styles.codeInput} style={styles.codeInput}
placeholder="Enter Game Code" placeholder="Enter Game Code"
@@ -154,6 +222,7 @@ const MapScreen = ({navigation}) => {
styleLabel={styles.joinLabel} styleLabel={styles.joinLabel}
/> />
</View> </View>
<View style={styles.fullRow}>
<ButtonTray> <ButtonTray>
<Button <Button
label="Create a Game" label="Create a Game"
@@ -162,29 +231,170 @@ const MapScreen = ({navigation}) => {
styleLabel={styles.createLabel} styleLabel={styles.createLabel}
/> />
</ButtonTray> </ButtonTray>
</View>
</Screen> </Screen>
); );
} }
// Admin view // Admin — creating / editing a cache
if (isAdmin && isCreating) {
return (
<Screen style={styles.containerMap}>
<View style={styles.mapWrap}>
<MapView
style={{flex: 1}}
initialRegion={newCoord ? {...newCoord, latitudeDelta: 0.01, longitudeDelta: 0.01} : mapRegion}
scrollEnabled={true}
zoomEnabled={true}
onPress={(e) => setNewCoord(e.nativeEvent.coordinate)}
showsUserLocation
>
{cacheRecords.map((cache) => (
<React.Fragment key={cache.id}>
<Marker coordinate={cache.coordinates} pinColor="#9ca3af" title={cache.clue}/>
<Circle
center={cache.coordinates}
radius={cache.radius}
fillColor="rgba(156,163,175,0.15)"
strokeColor="rgba(156,163,175,0.6)"
/>
</React.Fragment>
))}
{newCoord && (
<>
<Marker
coordinate={newCoord}
draggable
tracksViewChanges={false}
pinColor="#2563eb"
title={editingCacheId ? 'Edit Cache' : 'New Cache'}
onDragEnd={(e) => setNewCoord(e.nativeEvent.coordinate)}
/>
<Circle
center={newCoord}
radius={parseInt(newRadius) || 20}
fillColor="rgba(37,99,235,0.15)"
strokeColor="rgba(37,99,235,0.85)"
/>
</>
)}
</MapView>
</View>
<ScrollView style={styles.formSection}>
<TextInput
style={styles.formInput}
placeholder="Cache's Clue"
value={newClue}
onChangeText={setNewClue}
/>
<View style={styles.coordRow}>
<View style={styles.coordField}>
<Text style={styles.coordLabel}>Latitude</Text>
<TextInput
style={styles.formInput}
placeholder="Latitude"
value={newCoord ? String(newCoord.latitude.toFixed(6)) : ''}
editable={false}
/>
</View>
<View style={styles.coordField}>
<Text style={styles.coordLabel}>Longitude</Text>
<TextInput
style={styles.formInput}
placeholder="Longitude"
value={newCoord ? String(newCoord.longitude.toFixed(6)) : ''}
editable={false}
/>
</View>
</View>
<TextInput
style={styles.formInput}
placeholder="Claim Radius (metres)"
value={newRadius}
onChangeText={setNewRadius}
keyboardType="numeric"
/>
<ButtonTray>
<Button
label={editingCacheId ? 'Update Cache' : 'Save Cache'}
onClick={handleSaveCache}
styleButton={styles.saveButton}
styleLabel={styles.saveLabel}
/>
<Button
label="Cancel"
onClick={handleCancelCreate}
styleButton={styles.cancelButton}
styleLabel={styles.cancelLabel}
/>
</ButtonTray>
</ScrollView>
</Screen>
);
}
// Admin — normal view (map + create button + cache list)
if (isAdmin) { if (isAdmin) {
return ( return (
<Screen style={styles.center}> <Screen style={styles.containerMap}>
<Text style={styles.body}>Admins manage caches from Manage Game.</Text> <View style={styles.mapWrap}>
<MapView
style={{flex: 1}}
initialRegion={mapRegion}
showsUserLocation
>
{cacheRecords.map((cache) => (
<React.Fragment key={cache.id}>
<Marker coordinate={cache.coordinates} title={cache.clue}/>
<Circle
center={cache.coordinates}
radius={cache.radius}
fillColor="rgba(59,130,246,0.15)"
strokeColor="rgba(59,130,246,0.85)"
/>
</React.Fragment>
))}
</MapView>
</View>
<View style={styles.createCacheWrap}>
<Button
label="Create Cache"
onClick={handleCreateCachePress}
styleButton={styles.createCacheButton}
styleLabel={styles.createCacheLabel}
/>
</View>
<View style={styles.cacheSection}>
<ScrollView>
{cacheRecords.map((cache) => (
<CacheCardItem
key={cache.id}
cache={cache}
isAdmin={true}
onEdit={handleEditCache}
onDelete={handleDeleteCache}
/>
))}
{cacheRecords.length === 0 && (
<Text style={styles.emptyText}>No caches yet. Tap Create Cache to add one.</Text>
)}
</ScrollView>
</View>
</Screen> </Screen>
); );
} }
// Loading // Player — loading location
if (loading) { if (playerLoading) {
return ( return (
<Screen style={styles.center}> <Screen style={styles.center}>
<ActivityIndicator size="large"/> <ActivityIndicator size="large"/>
<Text style={styles.loadingText}>Getting your location...</Text>
</Screen> </Screen>
); );
} }
// Error // Player — error
if (error) { if (error) {
return ( return (
<Screen style={styles.center}> <Screen style={styles.center}>
@@ -212,12 +422,12 @@ const MapScreen = ({navigation}) => {
<View style={styles.cacheSection}> <View style={styles.cacheSection}>
<ScrollView> <ScrollView>
{cacheRecords.map((cache) => ( {cacheRecords.map((cache) => (
<View key={cache.id} style={styles.cacheCard}> <CacheCardItem
<Text style={styles.cacheClue}>{cache.clue}</Text> key={cache.id}
<Text style={[styles.cacheStatus, cache.ClaimedByUid && styles.cacheStatusClaimed]}> cache={cache}
{cache.ClaimedByUid ? 'Claimed' : 'Available'} isAdmin={false}
</Text> onSelect={handleSelectCache}
</View> />
))} ))}
{cacheRecords.length === 0 && ( {cacheRecords.length === 0 && (
<Text style={styles.emptyText}>No caches available yet.</Text> <Text style={styles.emptyText}>No caches available yet.</Text>
@@ -231,10 +441,11 @@ const MapScreen = ({navigation}) => {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
center: {justifyContent: 'center', alignItems: 'center'}, center: {justifyContent: 'center', alignItems: 'center'},
containerMap: {padding: 0}, containerMap: {padding: 0},
mapWrap: {flex: 2}, mapWrap: {flex: 1},
body: {color: '#4b5563', fontSize: 15},
error: {color: '#dc2626', fontSize: 15}, error: {color: '#dc2626', fontSize: 15},
row: {flexDirection: 'row', gap: 10, marginBottom: 15, width: '100%', paddingHorizontal: 20}, loadingText: {color: '#6b7280', fontSize: 14, marginTop: 10},
inputRow: {flexDirection: 'row', gap: 10, marginBottom: 15, width: '100%', paddingHorizontal: 20},
fullRow: {width: '100%', paddingHorizontal: 20},
codeInput: { codeInput: {
flex: 1, flex: 1,
borderWidth: 1, borderWidth: 1,
@@ -255,26 +466,33 @@ const styles = StyleSheet.create({
paddingVertical: 8, paddingVertical: 8,
fontSize: 14, fontSize: 14,
}, },
createCacheWrap: {paddingHorizontal: 12, paddingVertical: 8},
createCacheButton: {backgroundColor: '#2563eb', borderColor: '#2563eb'},
createCacheLabel: {color: '#ffffff', fontWeight: '600'},
formSection: {flex: 1, paddingHorizontal: 12, paddingVertical: 10},
formInput: {
borderWidth: 1,
borderColor: '#d1d5db',
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 10,
fontSize: 16,
backgroundColor: '#ffffff',
marginBottom: 10,
},
coordRow: {flexDirection: 'row', gap: 10},
coordField: {flex: 1},
coordLabel: {fontSize: 12, fontWeight: '600', color: '#6b7280', marginBottom: 4},
saveButton: {backgroundColor: '#16a34a', borderColor: '#16a34a'},
saveLabel: {color: '#ffffff', fontWeight: '600'},
cancelButton: {backgroundColor: '#6b7280', borderColor: '#6b7280'},
cancelLabel: {color: '#ffffff', fontWeight: '600'},
cacheSection: { cacheSection: {
flex: 1, maxHeight: 160,
backgroundColor: '#f3f4f6', backgroundColor: '#f3f4f6',
paddingHorizontal: 12, paddingHorizontal: 12,
paddingTop: 10, paddingTop: 10,
}, },
cacheCard: {
backgroundColor: '#ffffff',
borderRadius: 8,
padding: 12,
marginBottom: 8,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
borderWidth: 1,
borderColor: '#e5e7eb',
},
cacheClue: {fontSize: 15, fontWeight: '600', color: '#1f2937'},
cacheStatus: {fontSize: 13, color: '#16a34a', fontWeight: '600'},
cacheStatusClaimed: {color: '#9ca3af'},
emptyText: {color: '#9ca3af', textAlign: 'center', marginTop: 20, fontSize: 14}, emptyText: {color: '#9ca3af', textAlign: 'center', marginTop: 20, fontSize: 14},
}); });
+244
View File
@@ -0,0 +1,244 @@
import React, {useCallback, useEffect, useState} from 'react';
import {ActivityIndicator, Pressable, ScrollView, StyleSheet, Text, View} from 'react-native';
import Screen from '../layout/Screen';
import Card from '../UI/Card';
import {Button} from '../UI/Button';
import useGameHook from '../../hooks/useGameHook';
import {getSession} from '../../hooks/SessionStore';
const PlayersScreen = () => {
// Initialisation ------------
const session = getSession();
const {getLobby, getTeams, getTeamMembers, getGroupMembers, getUser, getCaches} = useGameHook();
// State ----------------------
const [loading, setLoading] = useState(true);
const [groupInfo, setGroupInfo] = useState(null);
const [teams, setTeams] = useState([]);
const [players, setPlayers] = useState([]);
const [selectedTeam, setSelectedTeam] = useState(null);
const [teamMembers, setTeamMembers] = useState([]);
const [memberNames, setMemberNames] = useState({});
const [memberCacheCounts, setMemberCacheCounts] = useState({});
const [detailLoading, setDetailLoading] = useState(false);
// Handlers -------------------
const loadData = useCallback(async () => {
if (!session.currentGid) {
setLoading(false);
return;
}
const group = await getLobby(session.currentGid);
setGroupInfo(group);
if (group && group.TeamsEnabled) {
// Teams enabled — load all teams for this group
const teamRows = await getTeams(session.currentGid);
setTeams(teamRows || []);
} else {
// Teams disabled — load all members for this group
const memberships = await getGroupMembers(session.currentGid);
const nonAdminMembers = (memberships || []).filter((m) => !m.IsAcceptedAdmin);
const playerList = [];
for (const m of nonAdminMembers) {
const user = await getUser(m.Uid);
if (user) playerList.push(user);
}
setPlayers(playerList);
}
setLoading(false);
}, [session.currentGid]);
useEffect(() => { loadData(); }, [loadData]);
const handleSelectTeam = async (team) => {
setSelectedTeam(team);
setDetailLoading(true);
// Load team members and their usernames
const ms = await getTeamMembers(team.Tid);
setTeamMembers(ms || []);
const names = {};
for (const m of (ms || [])) {
const user = await getUser(m.Uid);
if (user) names[m.Uid] = user.username;
}
setMemberNames(names);
// Load all caches to count individual contributions
const caches = await getCaches(session.currentGid, null);
const counts = {};
for (const m of (ms || [])) {
counts[m.Uid] = (caches || []).filter((c) => c.ClaimedByUid === m.Uid).length;
}
setMemberCacheCounts(counts);
setDetailLoading(false);
};
const handleBackToTeams = () => {
setSelectedTeam(null);
setTeamMembers([]);
setMemberNames({});
setMemberCacheCounts({});
};
// View -----------------------
if (loading) {
return (
<Screen style={styles.center}>
<ActivityIndicator size="large"/>
</Screen>
);
}
if (!session.currentGid) {
return (
<Screen style={styles.center}>
<Text style={styles.body}>Create or join a game first.</Text>
</Screen>
);
}
// Teams enabled — show team detail view
if (groupInfo?.TeamsEnabled && selectedTeam) {
return (
<Screen style={styles.container}>
<View style={styles.teamDetailHeader}>
<Text style={styles.teamDetailName}>{selectedTeam.TeamName || 'Unnamed Team'}</Text>
<Text style={styles.teamDetailCode}>Code: {selectedTeam.JoinCode}</Text>
</View>
{detailLoading ? (
<View style={styles.center}>
<ActivityIndicator size="large"/>
</View>
) : (
<ScrollView style={styles.listSection}>
<Text style={styles.sectionTitle}>Team Members</Text>
{teamMembers.map((m) => (
<Card key={m.id}>
<View style={styles.memberRow}>
<Text style={styles.memberName}>
{memberNames[m.Uid] || `Player ${m.Uid}`}
</Text>
<Text style={styles.memberCaches}>
{memberCacheCounts[m.Uid] || 0} cache(s) claimed
</Text>
</View>
</Card>
))}
{teamMembers.length === 0 && (
<Text style={styles.emptyText}>No members in this team.</Text>
)}
</ScrollView>
)}
<View style={styles.backWrap}>
<Button
label="Back to Teams"
onClick={handleBackToTeams}
styleButton={styles.backButton}
styleLabel={styles.backLabel}
/>
</View>
</Screen>
);
}
// Teams enabled — show team cards
if (groupInfo?.TeamsEnabled) {
return (
<Screen style={styles.container}>
<Text style={styles.sectionTitle}>Teams</Text>
<ScrollView style={styles.listSection}>
{teams.map((team) => (
<Pressable key={team.Tid} onPress={() => handleSelectTeam(team)}>
<Card>
<View style={styles.teamCardRow}>
<Text style={styles.teamName}>
{team.TeamName || 'Unnamed Team'}
</Text>
<Text style={styles.teamCode}>
Code: {team.JoinCode}
</Text>
</View>
</Card>
</Pressable>
))}
{teams.length === 0 && (
<Text style={styles.emptyText}>No teams have been created yet.</Text>
)}
</ScrollView>
</Screen>
);
}
// Teams disabled — show player list
return (
<Screen style={styles.container}>
<Text style={styles.sectionTitle}>Players</Text>
<ScrollView style={styles.listSection}>
{players.map((player) => (
<Card key={player.Uid}>
<Text style={styles.playerName}>{player.username}</Text>
</Card>
))}
{players.length === 0 && (
<Text style={styles.emptyText}>No players have joined yet.</Text>
)}
</ScrollView>
</Screen>
);
};
const styles = StyleSheet.create({
center: {justifyContent: 'center', alignItems: 'center', flex: 1},
container: {padding: 0},
body: {color: '#4b5563', fontSize: 15},
sectionTitle: {
fontSize: 20,
fontWeight: '700',
color: '#1f2937',
marginBottom: 10,
paddingHorizontal: 15,
paddingTop: 15,
},
listSection: {flex: 1, paddingHorizontal: 15},
emptyText: {color: '#9ca3af', textAlign: 'center', marginTop: 30, fontSize: 14},
// Team cards
teamCardRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
teamName: {fontSize: 16, fontWeight: '600', color: '#1f2937'},
teamCode: {fontSize: 14, fontWeight: '700', color: '#2563eb', letterSpacing: 1},
// Team detail
teamDetailHeader: {
backgroundColor: '#374151',
paddingVertical: 14,
paddingHorizontal: 16,
},
teamDetailName: {fontSize: 18, fontWeight: '700', color: '#ffffff', marginBottom: 4},
teamDetailCode: {fontSize: 14, fontWeight: '600', color: '#93c5fd', letterSpacing: 1},
memberRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
memberName: {fontSize: 16, fontWeight: '600', color: '#1f2937'},
memberCaches: {fontSize: 13, fontWeight: '600', color: '#16a34a'},
backWrap: {padding: 15},
backButton: {backgroundColor: '#6b7280', borderColor: '#6b7280'},
backLabel: {color: '#ffffff', fontWeight: '600'},
// Player list
playerName: {fontSize: 16, fontWeight: '600', color: '#1f2937'},
});
export default PlayersScreen;
+8 -2
View File
@@ -9,6 +9,7 @@ const TeamScreen = () => {
// Initialisation ------------ // Initialisation ------------
const session = getSession(); const session = getSession();
const isAdmin = session.isAcceptedAdmin;
const {getTeam, createTeam, joinTeamByCode, getTeamMembers, leaveTeam, getUser, updateUser} = useGameHook(); const {getTeam, createTeam, joinTeamByCode, getTeamMembers, leaveTeam, getUser, updateUser} = useGameHook();
// State ---------------------- // State ----------------------
@@ -86,7 +87,7 @@ const TeamScreen = () => {
if (!activeTid || !team) { if (!activeTid || !team) {
return ( return (
<Screen style={styles.center}> <Screen style={styles.center}>
<View style={styles.row}> <View style={styles.inputRow}>
<TextInput <TextInput
style={styles.codeInput} style={styles.codeInput}
placeholder="Enter Team Code" placeholder="Enter Team Code"
@@ -101,6 +102,8 @@ const TeamScreen = () => {
styleLabel={styles.joinLabel} styleLabel={styles.joinLabel}
/> />
</View> </View>
{!isAdmin && (
<View style={styles.fullRow}>
<ButtonTray> <ButtonTray>
<Button <Button
label="Create Team" label="Create Team"
@@ -109,6 +112,8 @@ const TeamScreen = () => {
styleLabel={styles.createLabel} styleLabel={styles.createLabel}
/> />
</ButtonTray> </ButtonTray>
</View>
)}
</Screen> </Screen>
); );
} }
@@ -145,7 +150,8 @@ const TeamScreen = () => {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
center: {justifyContent: 'center', alignItems: 'center'}, center: {justifyContent: 'center', alignItems: 'center'},
container: {padding: 0}, container: {padding: 0},
row: {flexDirection: 'row', gap: 10, marginBottom: 15, width: '100%', paddingHorizontal: 20}, inputRow: {flexDirection: 'row', gap: 10, marginBottom: 15, width: '100%', paddingHorizontal: 20},
fullRow: {width: '100%', paddingHorizontal: 20},
codeInput: { codeInput: {
flex: 1, flex: 1,
borderWidth: 1, borderWidth: 1,
+31
View File
@@ -6,6 +6,7 @@ const useGameHook = () => {
const usersEndpoint = `${API_BASE_URL}/users`; const usersEndpoint = `${API_BASE_URL}/users`;
const gameTypesEndpoint = `${API_BASE_URL}/game-types`; const gameTypesEndpoint = `${API_BASE_URL}/game-types`;
const groupsEndpoint = `${API_BASE_URL}/groups`; const groupsEndpoint = `${API_BASE_URL}/groups`;
const subgroupsEndpoint = `${API_BASE_URL}/subgroups`;
const subgroupMembershipsEndpoint = `${API_BASE_URL}/subgroup-memberships`; const subgroupMembershipsEndpoint = `${API_BASE_URL}/subgroup-memberships`;
const teamsEndpoint = `${API_BASE_URL}/teams`; const teamsEndpoint = `${API_BASE_URL}/teams`;
const teamMembersEndpoint = `${API_BASE_URL}/team-members`; const teamMembersEndpoint = `${API_BASE_URL}/team-members`;
@@ -47,18 +48,38 @@ const useGameHook = () => {
return response.isSuccess ? response.result : null; return response.isSuccess ? response.result : null;
}; };
const updateGroup = async (gid, data) => {
const response = await API.put(`${groupsEndpoint}/${gid}`, data);
return response.isSuccess ? response.result : null;
};
const getSubgroups = async (gid) => {
const response = await API.get(`${subgroupsEndpoint}?Gid=${gid}`);
return response.isSuccess ? response.result : [];
};
// Subgroup memberships // Subgroup memberships
const joinPrivateGame = async (payload) => { const joinPrivateGame = async (payload) => {
const response = await API.post(subgroupMembershipsEndpoint, payload); const response = await API.post(subgroupMembershipsEndpoint, payload);
return response.isSuccess ? response.result : null; return response.isSuccess ? response.result : null;
}; };
const getGroupMembers = async (gid) => {
const response = await API.get(`${subgroupMembershipsEndpoint}?Gid=${gid}`);
return response.isSuccess ? response.result : [];
};
const joinAsAdmin = async (payload) => { const joinAsAdmin = async (payload) => {
const response = await API.post(adminWaitlistEndpoint, payload); const response = await API.post(adminWaitlistEndpoint, payload);
return response.isSuccess ? response.result : null; return response.isSuccess ? response.result : null;
}; };
// Teams // Teams
const getTeams = async (gid) => {
const response = await API.get(`${teamsEndpoint}?Gid=${gid}`);
return response.isSuccess ? response.result : [];
};
const getTeam = async (tid) => { const getTeam = async (tid) => {
const response = await API.get(`${teamsEndpoint}/${tid}`); const response = await API.get(`${teamsEndpoint}/${tid}`);
return response.isSuccess ? response.result : null; return response.isSuccess ? response.result : null;
@@ -116,6 +137,11 @@ const useGameHook = () => {
return response.isSuccess ? response.result : null; return response.isSuccess ? response.result : null;
}; };
const deleteCache = async (gid, cacheId) => {
const response = await API.delete(`${gameDataEndpoint}/${gid}/caches/${cacheId}`);
return response.isSuccess;
};
// Return --------------------- // Return ---------------------
return { return {
@@ -125,8 +151,12 @@ const useGameHook = () => {
getCreatedPrivateGame, getCreatedPrivateGame,
createPrivateGame, createPrivateGame,
getLobby, getLobby,
updateGroup,
getSubgroups,
joinPrivateGame, joinPrivateGame,
joinAsAdmin, joinAsAdmin,
getGroupMembers,
getTeams,
getTeam, getTeam,
createTeam, createTeam,
joinTeamByCode, joinTeamByCode,
@@ -135,6 +165,7 @@ const useGameHook = () => {
getCaches, getCaches,
upsertCache, upsertCache,
claimCache, claimCache,
deleteCache,
}; };
}; };