Refactor session management to use AsyncStorage for persistence; update game settings and leaderboard screens for improved functionality

This commit is contained in:
bobbert committed 2026-04-08 16:45:00 +01:00
1 parent 2646c7b9bc
commit 5183039b7b
26 files changed
+636 -849

No files matched your search

+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://192.168.8.100:3000'; //For when im home (API Endpoint is on my home server...)
//export const API_BASE_URL = 'http://api.bobby.ip-ddns.com'; // For actual use
export const API_BASE_URL = 'http://192.168.8.100:3000'; // Home network for mudabbir since, server is on my home network and router dont do loopback!!!
const API = {};
API.get = (endpoint) => callFetch(endpoint, 'GET');
@@ -1,48 +0,0 @@
import React from 'react';
import MapView, {Circle, Marker} from 'react-native-maps';
const AdminCacheEditorView = ({ userLocation, caches, claimDistance, onAddCache, onMoveCache }) => {
// Initialisation -------------
// State ----------------------
// Handlers -------------------
// View -----------------------
if (!userLocation) {
return null;
}
return (
<MapView
style={{ flex: 1 }}
initialRegion={{
...userLocation,
latitudeDelta: 0.01,
longitudeDelta: 0.01,
}}
onLongPress={(event) => onAddCache(event.nativeEvent.coordinate)}
scrollEnabled={true}
zoomEnabled={true}
showsUserLocation
>
{(caches || []).map((cache) => (
<React.Fragment key={cache.id}>
<Marker
coordinate={cache.coordinates}
draggable
onDragEnd={(event) => onMoveCache(cache.id, event.nativeEvent.coordinate)}
title={`Cache ${cache.id}`}
/>
<Circle
center={cache.coordinates}
radius={claimDistance}
fillColor="rgba(59, 130, 246, 0.15)"
strokeColor="rgba(59, 130, 246, 0.85)"
/>
</React.Fragment>
))}
</MapView>
);
};
export default AdminCacheEditorView;
+33 -39
View File
@@ -1,52 +1,46 @@
import React from 'react';
import MapView, {Circle, Polygon} from 'react-native-maps';
import {getFovCone} from '../../utils/geoMath';
const PlayerMapView = ({ userLocation, visibleCache, heading, claimDistance }) => {
const PlayerMapView = ({userLocation, visibleCache, heading, claimDistance}) => {
// Initialisation -------------
// State ----------------------
// Handlers -------------------
// View -----------------------
if (!userLocation) return null;
if (!userLocation) return null;
const coneCoords = (heading !== null && heading !== undefined)
? getFovCone(userLocation, heading)
: null;
const coneCoords = (heading !== null && heading !== undefined)
? getFovCone(userLocation, heading)
: null;
return (
<MapView
style={{flex: 1}}
region={{
...userLocation,
latitudeDelta: 0.01,
longitudeDelta: 0.01,
}}
scrollEnabled={false}
zoomEnabled={true}
rotateEnabled={false}
pitchEnabled={false}
showsUserLocation
>
{visibleCache ? (
<Circle
center={visibleCache.coordinates}
radius={claimDistance}
fillColor="rgba(250, 204, 21, 0.20)"
strokeColor="rgba(250, 204, 21, 0.90)"
/>
) : null}
{coneCoords && (
<Polygon
coordinates={coneCoords}
fillColor="rgba(66,133,244,0.28)"
strokeColor="rgba(66,133,244,0.50)"
strokeWidth={1}
/>
)}
</MapView>
);
return (
<MapView
style={{flex: 1}}
region={{...userLocation, latitudeDelta: 0.01, longitudeDelta: 0.01}}
scrollEnabled={false}
zoomEnabled={true}
rotateEnabled={false}
pitchEnabled={false}
showsUserLocation
>
{visibleCache && (
<Circle
center={visibleCache.coordinates}
radius={claimDistance}
fillColor="rgba(250, 204, 21, 0.20)"
strokeColor="rgba(250, 204, 21, 0.90)"
/>
)}
{coneCoords && (
<Polygon
coordinates={coneCoords}
fillColor="rgba(66,133,244,0.28)"
strokeColor="rgba(66,133,244,0.50)"
strokeWidth={1}
/>
)}
</MapView>
);
};
export default PlayerMapView;
+45 -41
View File
@@ -1,71 +1,69 @@
import {Pressable, StyleSheet, Text, View} from 'react-native';
import {Alert, Pressable, StyleSheet, Text, View} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {getSession} from '../../hooks/SessionStore';
import {clearGameSession, getSession} from '../../hooks/SessionStore';
import API, {API_BASE_URL} from '../API/API';
const BottomNavbar = ({ navigation, routeName }) => {
const BottomNavbar = ({navigation, routeName}) => {
// Initialisation ------------
const insets = useSafeAreaInsets();
const session = getSession();
const adminTabs = [
{
label: 'Game Settings',
route: 'GameSettingsScreen',
onPress: () => navigation.navigate('GameSettingsScreen'),
},
{
label: 'Map',
route: 'MapScreen',
onPress: () => navigation.navigate('MapScreen'),
},
{
label: 'Players',
route: 'PlayersScreen',
onPress: () => navigation.navigate('PlayersScreen'),
},
{
label: 'Leaderboard',
route: 'LeaderboardScreen',
onPress: () => navigation.navigate('LeaderboardScreen'),
},
{label: 'Settings', route: 'GameSettingsScreen', onPress: () => navigation.navigate('GameSettingsScreen')},
{label: 'Map', route: 'MapScreen', onPress: () => navigation.navigate('MapScreen')},
{label: 'Players', route: 'PlayersScreen', onPress: () => navigation.navigate('PlayersScreen')},
{label: 'Leaderboard', route: 'LeaderboardScreen', onPress: () => navigation.navigate('LeaderboardScreen')},
];
const playerTabs = [
{
label: 'Map',
route: 'MapScreen',
onPress: () => navigation.navigate('MapScreen'),
},
{
label: 'Leaderboard',
route: 'LeaderboardScreen',
onPress: () => navigation.navigate('LeaderboardScreen'),
},
{
label: 'Current Team',
route: 'TeamScreen',
onPress: () => navigation.navigate('TeamScreen'),
},
{label: 'Team', route: 'TeamScreen', onPress: () => navigation.navigate('TeamScreen')},
{label: 'Map', route: 'MapScreen', onPress: () => navigation.navigate('MapScreen')},
{label: 'Leaderboard', route: 'LeaderboardScreen', onPress: () => navigation.navigate('LeaderboardScreen')},
{label: 'Leave', route: '__leave__', onPress: () => handleLeaveGame()},
];
const tabs = session.isAcceptedAdmin ? adminTabs : playerTabs;
// State ----------------------
// Handlers -------------------
const handleLeaveGame = () => {
Alert.alert('Leave Game', 'Are you sure you want to leave this game?', [
{text: 'Cancel', style: 'cancel'},
{
text: 'Leave',
style: 'destructive',
onPress: async () => {
const s = getSession();
const endpoint = `${API_BASE_URL}/subgroup-memberships?Uid=${s.currentUid}&Gid=${s.currentGid}`;
const res = await API.get(endpoint);
if (res.isSuccess && res.result.length > 0) {
await API.delete(`${API_BASE_URL}/subgroup-memberships/${res.result[0].id}`);
}
clearGameSession();
navigation.reset({index: 0, routes: [{name: 'Game'}]});
},
},
]);
};
// View -----------------------
return (
<View style={[styles.container, {paddingBottom: insets.bottom}]}>
{tabs.map((tab) => {
const isActive = routeName === tab.route;
const isLeave = tab.route === '__leave__';
return (
<Pressable
key={tab.route}
key={tab.label}
onPress={tab.onPress}
style={[styles.tab, isActive && styles.tabActive]}
style={[styles.tab, isActive && styles.tabActive, isLeave && styles.tabLeave]}
>
<Text style={[styles.tabText, isActive && styles.tabTextActive]}>{tab.label}</Text>
<Text style={[styles.tabText, isActive && styles.tabTextActive, isLeave && styles.tabTextLeave]}>
{tab.label}
</Text>
</Pressable>
);
})}
@@ -95,6 +93,9 @@ const styles = StyleSheet.create({
tabActive: {
backgroundColor: '#5c5c5c',
},
tabLeave: {
backgroundColor: 'rgba(220,38,38,0.15)',
},
tabText: {
color: '#d1d5db',
fontSize: 12,
@@ -104,6 +105,9 @@ const styles = StyleSheet.create({
tabTextActive: {
color: '#ffffff',
},
tabTextLeave: {
color: '#fca5a5',
},
});
export default BottomNavbar;
-8
View File
@@ -13,17 +13,9 @@ const Screen = ({ children, style, showBack = false }) => {
const insets = useSafeAreaInsets();
const titleMap = {
GameTypeScreen: "Game Type",
JoinPrivateGameScreen: "Join Private Game",
ManagePrivateGameScreen: "Manage Private Game",
CreateGameScreen: "Create Game",
ManageGameScreen: "Manage Game",
GameLobbyScreen: "Game Lobby",
LeaderboardScreen: "Leaderboard",
TeamScreen: "Current Team",
MapScreen: "Map",
AdminHubScreen: "Admin Hub",
ManageSubgroupsScreen: "Manage Subgroups",
GameSettingsScreen: "Game Settings",
PlayersScreen: "Players",
ExpandedMapScreen: "Map View",
-25
View File
@@ -1,25 +0,0 @@
import React from 'react';
import {StyleSheet, Text} from 'react-native';
import Screen from '../layout/Screen';
const AdminHubScreen = () => {
// Initialisation -------------
// State ----------------------
// Handlers -------------------
// View -----------------------
return (
<Screen showBack={true} 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({
container: {justifyContent: 'center', alignItems: 'center'},
title: {fontSize: 24, fontWeight: 'bold'},
body: {color: '#4b5563', fontSize: 15, marginTop: 10},
});
export default AdminHubScreen;
@@ -1,25 +0,0 @@
import React from 'react';
import {StyleSheet, Text} from 'react-native';
import Screen from '../layout/Screen';
const CreateGameScreen = () => {
// Initialisation -------------
// State ----------------------
// Handlers -------------------
// View -----------------------
return (
<Screen showBack={true} 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({
container: {justifyContent: 'center', alignItems: 'center'},
title: {fontSize: 24, fontWeight: 'bold'},
body: {color: '#4b5563', fontSize: 15, marginTop: 10},
});
export default CreateGameScreen;
+20 -9
View File
@@ -2,22 +2,22 @@ import React, {useEffect, useState} from 'react';
import {StyleSheet, View} from 'react-native';
import MapView, {Circle, Marker, Polygon} from 'react-native-maps';
import * as Location from 'expo-location';
import {Magnetometer} from 'expo-sensors';
import Screen from '../layout/Screen';
import {getFovCone, toHeading} from '../../utils/geoMath';
import {getFovCone} from '../../utils/geoMath';
const DEFAULT_REGION = {latitude: 51.5074, longitude: -0.1278, latitudeDelta: 0.01, longitudeDelta: 0.01};
const ExpandedMapScreen = ({route}) => {
// Initialisation ------------
const {isAdmin, cacheRecords: cacheStr, claimDistance: routeClaimDistance} = route.params || {};
const {isAdmin, cacheRecords: cacheStr, claimDistance: routeClaimDistance, userLocation: routeLocation} = route.params || {};
const caches = cacheStr ? JSON.parse(cacheStr) : [];
const claimDistance = routeClaimDistance || 20;
// State ----------------------
const [userLocation, setUserLocation] = useState(null);
// Seed with the location passed from MapScreen so the map opens centred on the user immediately
const [userLocation, setUserLocation] = useState(routeLocation || null);
const [heading, setHeading] = useState(null);
// Handlers -------------------
@@ -30,16 +30,27 @@ const ExpandedMapScreen = ({route}) => {
const {status} = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') return;
const pos = await Location.getCurrentPositionAsync({accuracy: Location.Accuracy.Balanced});
setUserLocation({latitude: pos.coords.latitude, longitude: pos.coords.longitude});
// Only do a fresh one-shot fix if MapScreen didn't supply a location
if (!routeLocation) {
const last = await Location.getLastKnownPositionAsync();
if (last) {
setUserLocation({latitude: last.coords.latitude, longitude: last.coords.longitude});
} else {
const pos = await Location.getCurrentPositionAsync({accuracy: Location.Accuracy.Low});
setUserLocation({latitude: pos.coords.latitude, longitude: pos.coords.longitude});
}
}
locationSub = await Location.watchPositionAsync(
{accuracy: Location.Accuracy.Balanced, distanceInterval: 2, timeInterval: 1000},
(next) => setUserLocation({latitude: next.coords.latitude, longitude: next.coords.longitude}),
);
Magnetometer.setUpdateInterval(500);
headingSub = Magnetometer.addListener((data) => setHeading(toHeading(data)));
// Use OS-fused heading from expo-location
headingSub = await Location.watchHeadingAsync((headingData) => {
const raw = headingData.trueHeading >= 0 ? headingData.trueHeading : headingData.magHeading;
setHeading(raw);
});
};
start();
@@ -75,7 +86,7 @@ const ExpandedMapScreen = ({route}) => {
title={cache.name || cache.clue}
pinColor="#2563eb"
/>
<Circle
<Circle
center={cache.coordinates}
radius={claimDistance}
fillColor="rgba(59,130,246,0.15)"
-25
View File
@@ -1,25 +0,0 @@
import React from 'react';
import {StyleSheet, Text} from 'react-native';
import Screen from '../layout/Screen';
const GameLobbyScreen = () => {
// Initialisation -------------
// State ----------------------
// Handlers -------------------
// View -----------------------
return (
<Screen showBack={true} 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({
container: {justifyContent: 'center', alignItems: 'center'},
title: {fontSize: 24, fontWeight: 'bold'},
body: {color: '#4b5563', fontSize: 15, marginTop: 10},
});
export default GameLobbyScreen;
+9 -4
View File
@@ -3,7 +3,7 @@ import {ActivityIndicator, ScrollView, StyleSheet, Switch, Text, TextInput, View
import Screen from '../layout/Screen';
import {Button, ButtonTray} from '../UI/Button';
import useGameHook from '../../hooks/useGameHook';
import {getSession} from '../../hooks/SessionStore';
import {getSession, setSessionTeamsEnabled} from '../../hooks/SessionStore';
const GameSettingsScreen = () => {
// Initialisation ------------
@@ -17,7 +17,7 @@ const GameSettingsScreen = () => {
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [groupName, setGroupName] = useState('');
const [teamsEnabled, setTeamsEnabled] = useState(false);
const [teamsEnabled, setTeamsEnabled] = useState(session.teamsEnabled);
const [maxSubgroups, setMaxSubgroups] = useState('1');
const [cacheTriggerMeters, setCacheTriggerMeters] = useState('20');
const [adminJoinCode, setAdminJoinCode] = useState('');
@@ -34,7 +34,10 @@ const GameSettingsScreen = () => {
const group = await getLobby(session.currentGid);
if (group) {
setGroupName(group.GroupName || '');
setTeamsEnabled(Boolean(group.TeamsEnabled));
const serverTeams = Boolean(group.TeamsEnabled);
setTeamsEnabled(serverTeams);
// Keep session in sync so the toggle survives app reloads
setSessionTeamsEnabled(serverTeams);
setMaxSubgroups(String(group.MaxMemberSubgroups || 1));
setCacheTriggerMeters(String(group.CacheTriggerMeters || 20));
setAdminJoinCode(group.AdminJoinCode || '');
@@ -50,12 +53,14 @@ const GameSettingsScreen = () => {
const handleSave = async () => {
if (!session.currentGid) return;
setSaving(true);
await updateGroup(session.currentGid, {
const result = await updateGroup(session.currentGid, {
GroupName: groupName.trim(),
TeamsEnabled: teamsEnabled,
MaxMemberSubgroups: parseInt(maxSubgroups) || 1,
CacheTriggerMeters: parseInt(cacheTriggerMeters) || 20,
});
// Persist the new toggle value so the session reflects it after a reload
if (result) setSessionTeamsEnabled(teamsEnabled);
setSaving(false);
};
-73
View File
@@ -1,73 +0,0 @@
import React, {useEffect, useState} from 'react';
import {Pressable, StyleSheet, Text, View} from 'react-native';
import useGameHook from '../../hooks/useGameHook';
import Screen from '../layout/Screen';
const GameTypeScreen = ({navigation}) => {
// Initialisation ------------
const {getGameTypes} = useGameHook();
// State ----------------------
const [gameTypes, setGameTypes] = useState([]);
// Handlers -------------------
useEffect(() => {
let isMounted = true;
const load = async () => {
const types = await getGameTypes();
if (isMounted) setGameTypes(types || []);
};
load();
return () => { isMounted = false; };
}, []);
// View -----------------------
return (
<Screen showBack={true} 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({
container: {
justifyContent: 'center',
},
content: {
gap: 12,
flex: 1,
},
title: {
fontSize: 24,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 20,
},
button: {
backgroundColor: '#2563eb',
borderRadius: 8,
paddingVertical: 12,
alignItems: 'center',
},
buttonText: {
color: '#ffffff',
fontWeight: '600',
},
});
export default GameTypeScreen;
@@ -1,35 +0,0 @@
import React from 'react';
import {StyleSheet, Text} from 'react-native';
import Screen from '../layout/Screen';
const JoinPrivateGameScreen = () => {
// Initialisation -------------
// State ----------------------
// Handlers -------------------
// View -----------------------
return (
<Screen showBack={true} style={styles.container}>
<Text style={styles.title}>Join Private Game</Text>
<Text style={styles.body}>Join private game setup goes here.</Text>
</Screen>
);
};
const styles = StyleSheet.create({
container: {
justifyContent: 'center',
alignItems: 'center',
},
title: {
fontSize: 24,
fontWeight: 'bold',
},
body: {
color: '#4b5563',
fontSize: 15,
marginTop: 10,
},
});
export default JoinPrivateGameScreen;
+224 -8
View File
@@ -1,25 +1,241 @@
import React from 'react';
import {StyleSheet, Text} from 'react-native';
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 useGameHook from '../../hooks/useGameHook';
import {getSession} from '../../hooks/SessionStore';
const LeaderboardScreen = () => {
// Initialisation -------------
// Initialisation ------------
const session = getSession();
const {getLobby, getTeams, getTeamMembers, getGroupMembers, getCaches, getUser} = useGameHook();
// State ----------------------
const [loading, setLoading] = useState(true);
const [teamsEnabled, setTeamsEnabled] = useState(false);
const [activeTab, setActiveTab] = useState('global');
const [globalRanking, setGlobalRanking] = useState([]);
const [teamRanking, setTeamRanking] = useState([]);
const [expandedTeam, setExpandedTeam] = useState(null);
// Handlers -------------------
const loadData = useCallback(async () => {
if (!session.currentGid) { setLoading(false); return; }
const group = await getLobby(session.currentGid);
const isTeams = Boolean(group?.TeamsEnabled);
setTeamsEnabled(isTeams);
const caches = await getCaches(session.currentGid, null);
const claimedCaches = (caches || []).filter((c) => c.ClaimedByUid);
if (isTeams) {
// Build team rankings
const teams = await getTeams(session.currentGid);
const teamData = [];
for (const team of (teams || [])) {
const members = await getTeamMembers(team.Tid);
const memberData = [];
for (const m of (members || [])) {
const user = await getUser(m.Uid);
const count = claimedCaches.filter((c) => c.ClaimedByUid === m.Uid).length;
memberData.push({
uid: m.Uid,
name: user?.username || `Player ${m.Uid}`,
caches: count,
isLeader: Boolean(m.IsLeader),
});
}
memberData.sort((a, b) => b.caches - a.caches);
const totalCaches = memberData.reduce((sum, m) => sum + m.caches, 0);
teamData.push({
tid: team.Tid,
name: team.TeamName || 'Unnamed Team',
code: team.JoinCode,
totalCaches,
members: memberData,
});
}
teamData.sort((a, b) => b.totalCaches - a.totalCaches);
setGlobalRanking(teamData);
// Build current team ranking
if (session.currentTid) {
const myTeam = teamData.find((t) => t.tid === session.currentTid);
setTeamRanking(myTeam ? myTeam.members : []);
}
} else {
// Build player rankings (no teams)
const allMembers = await getGroupMembers(session.currentGid);
const playerData = [];
for (const m of (allMembers || []).filter((m) => !m.IsAcceptedAdmin)) {
const user = await getUser(m.Uid);
const count = claimedCaches.filter((c) => c.ClaimedByUid === m.Uid).length;
playerData.push({
uid: m.Uid,
name: user?.username || `Player ${m.Uid}`,
caches: count,
});
}
playerData.sort((a, b) => b.caches - a.caches);
setGlobalRanking(playerData);
}
setLoading(false);
}, [session.currentGid, session.currentTid]);
useEffect(() => { loadData(); }, [loadData]);
// View -----------------------
if (loading) {
return (
<Screen style={styles.center}>
<ActivityIndicator size="large"/>
</Screen>
);
}
if (!session.currentGid) {
return (
<Screen style={styles.center}>
<Text style={styles.body}>Join a game to view the leaderboard.</Text>
</Screen>
);
}
// Teams enabled — tabbed view
if (teamsEnabled) {
return (
<Screen style={styles.container}>
<View style={styles.tabRow}>
<Pressable
style={[styles.tabButton, activeTab === 'global' && styles.tabButtonActive]}
onPress={() => setActiveTab('global')}
>
<Text style={[styles.tabLabel, activeTab === 'global' && styles.tabLabelActive]}>Global</Text>
</Pressable>
<Pressable
style={[styles.tabButton, activeTab === 'team' && styles.tabButtonActive]}
onPress={() => setActiveTab('team')}
>
<Text style={[styles.tabLabel, activeTab === 'team' && styles.tabLabelActive]}>Team</Text>
</Pressable>
</View>
{activeTab === 'global' ? (
<ScrollView style={styles.listSection}>
{globalRanking.map((team, index) => (
<Pressable key={team.tid} onPress={() => setExpandedTeam(expandedTeam === team.tid ? null : team.tid)}>
<Card>
<View style={styles.rankRow}>
<Text style={styles.rank}>#{index + 1}</Text>
<Text style={styles.rankName}>{team.name}</Text>
<Text style={styles.score}>{team.totalCaches} cache(s)</Text>
</View>
{expandedTeam === team.tid && team.members.map((m) => (
<View key={m.uid} style={styles.subRow}>
<Text style={styles.subName}>
{m.name}{m.isLeader ? ' (Leader)' : ''}
</Text>
<Text style={styles.subScore}>{m.caches}</Text>
</View>
))}
</Card>
</Pressable>
))}
{globalRanking.length === 0 && (
<Text style={styles.emptyText}>No teams yet.</Text>
)}
</ScrollView>
) : (
<ScrollView style={styles.listSection}>
{teamRanking.map((m, index) => (
<Card key={m.uid}>
<View style={styles.rankRow}>
<Text style={styles.rank}>#{index + 1}</Text>
<Text style={styles.rankName}>
{m.name}{m.isLeader ? ' (Leader)' : ''}
</Text>
<Text style={styles.score}>{m.caches} cache(s)</Text>
</View>
</Card>
))}
{teamRanking.length === 0 && (
<Text style={styles.emptyText}>Join a team to see your team leaderboard.</Text>
)}
</ScrollView>
)}
</Screen>
);
}
// No teams — simple player ranking
return (
<Screen style={styles.container}>
<Text style={styles.title}>Leaderboard</Text>
<Text style={styles.body}>Leaderboard view goes here.</Text>
<ScrollView style={styles.listSection}>
{globalRanking.map((player, index) => (
<Card key={player.uid}>
<View style={styles.rankRow}>
<Text style={styles.rank}>#{index + 1}</Text>
<Text style={styles.rankName}>{player.name}</Text>
<Text style={styles.score}>{player.caches} cache(s)</Text>
</View>
</Card>
))}
{globalRanking.length === 0 && (
<Text style={styles.emptyText}>No players yet.</Text>
)}
</ScrollView>
</Screen>
);
};
const styles = StyleSheet.create({
container: {justifyContent: 'center', alignItems: 'center'},
title: {fontSize: 24, fontWeight: 'bold'},
body: {color: '#4b5563', fontSize: 15, marginTop: 10},
center: {justifyContent: 'center', alignItems: 'center'},
container: {padding: 0},
body: {color: '#4b5563', fontSize: 15},
tabRow: {
flexDirection: 'row',
borderBottomWidth: 1,
borderBottomColor: '#e5e7eb',
},
tabButton: {
flex: 1,
paddingVertical: 12,
alignItems: 'center',
},
tabButtonActive: {
borderBottomWidth: 2,
borderBottomColor: '#2563eb',
},
tabLabel: {fontSize: 15, fontWeight: '600', color: '#6b7280'},
tabLabelActive: {color: '#2563eb'},
listSection: {flex: 1, paddingHorizontal: 15, paddingTop: 10},
rankRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
},
rank: {fontSize: 16, fontWeight: '700', color: '#374151', width: 32},
rankName: {flex: 1, fontSize: 15, fontWeight: '600', color: '#1f2937'},
score: {fontSize: 14, fontWeight: '600', color: '#16a34a'},
subRow: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingLeft: 42,
paddingVertical: 6,
borderTopWidth: 1,
borderTopColor: '#f3f4f6',
},
subName: {fontSize: 13, color: '#4b5563'},
subScore: {fontSize: 13, fontWeight: '600', color: '#16a34a'},
emptyText: {color: '#9ca3af', textAlign: 'center', marginTop: 30, fontSize: 14},
});
export default LeaderboardScreen;
-151
View File
@@ -1,151 +0,0 @@
import React, {useCallback, useEffect, useState} from 'react';
import {ActivityIndicator, StyleSheet, Text, View} from 'react-native';
import * as Location from 'expo-location';
import Screen from '../layout/Screen';
import AdminCacheEditorView from '../gameplay/AdminCacheEditorView';
import useGameHook from '../../hooks/useGameHook';
import {getSession} from '../../hooks/SessionStore';
const ManageGameScreen = () => {
// Initialisation ------------
const session = getSession();
const {getCaches, upsertCache, getLobby} = useGameHook();
// State ----------------------
const [userLocation, setUserLocation] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [cacheRecords, setCacheRecords] = useState([]);
const [claimDistance, setClaimDistance] = useState(20);
// Handlers -------------------
const loadCaches = useCallback(async () => {
if (!session.currentGid) {
setCacheRecords([]);
return;
}
const rows = await getCaches(session.currentGid, null);
setCacheRecords(rows || []);
}, [session.currentGid]);
useEffect(() => { loadCaches(); }, [loadCaches]);
useEffect(() => {
if (!session.currentGid) return;
getLobby(session.currentGid).then((group) => {
if (group) setClaimDistance(group.CacheTriggerMeters || 20);
});
}, [session.currentGid]);
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,
clue: `Cache ${cacheRecords.length + 1}`,
subgroupId: 1,
});
await loadCaches();
};
const handleMoveCache = async (cacheId, coordinate) => {
if (!session.currentGid) return;
const current = cacheRecords.find((cache) => cache.id === cacheId);
if (!current) return;
await upsertCache({
gid: session.currentGid,
cacheId,
latitude: coordinate.latitude,
longitude: coordinate.longitude,
clue: current.clue,
subgroupId: current.subgroupId,
});
await loadCaches();
};
// View -----------------------
if (!session.currentGid) {
return (
<Screen showBack={true} style={styles.center}>
<Text style={styles.body}>Create or join a game first.</Text>
</Screen>
);
}
if (!session.isAcceptedAdmin) {
return (
<Screen showBack={true} style={styles.center}>
<Text style={styles.body}>Only admins can edit cache locations.</Text>
</Screen>
);
}
if (loading) {
return (
<Screen showBack={true} style={styles.center}>
<ActivityIndicator size="large"/>
</Screen>
);
}
if (error) {
return (
<Screen showBack={true} style={styles.center}>
<Text style={styles.error}>{error}</Text>
</Screen>
);
}
return (
<Screen showBack={true} style={styles.container}>
<View style={styles.mapWrap}>
<AdminCacheEditorView
userLocation={userLocation}
caches={cacheRecords}
claimDistance={claimDistance}
onAddCache={handleAddCache}
onMoveCache={handleMoveCache}
/>
</View>
</Screen>
);
};
const styles = StyleSheet.create({
container: {padding: 0},
mapWrap: {flex: 1},
center: {justifyContent: 'center', alignItems: 'center'},
body: {color: '#4b5563', fontSize: 15},
error: {color: '#dc2626', fontSize: 15},
});
export default ManageGameScreen;
@@ -1,25 +0,0 @@
import React from 'react';
import {StyleSheet, Text} from 'react-native';
import Screen from '../layout/Screen';
const ManagePrivateGameScreen = () => {
// Initialisation -------------
// State ----------------------
// Handlers -------------------
// View -----------------------
return (
<Screen showBack={true} 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({
container: {justifyContent: 'center', alignItems: 'center'},
title: {fontSize: 24, fontWeight: 'bold'},
body: {color: '#4b5563', fontSize: 15, marginTop: 10},
});
export default ManagePrivateGameScreen;
@@ -1,25 +0,0 @@
import React from 'react';
import {StyleSheet, Text} from 'react-native';
import Screen from '../layout/Screen';
const ManageSubgroupsScreen = () => {
// Initialisation -------------
// State ----------------------
// Handlers -------------------
// View -----------------------
return (
<Screen showBack={true} 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({
container: {justifyContent: 'center', alignItems: 'center'},
title: {fontSize: 24, fontWeight: 'bold'},
body: {color: '#4b5563', fontSize: 15, marginTop: 10},
});
export default ManageSubgroupsScreen;
+21 -11
View File
@@ -2,7 +2,6 @@ import React, {useCallback, useEffect, useState} from 'react';
import {ActivityIndicator, Pressable, ScrollView, StyleSheet, Text, TextInput, View} from 'react-native';
import MapView, {Circle, Marker, Polygon} from 'react-native-maps';
import * as Location from 'expo-location';
import {Magnetometer} from 'expo-sensors';
import Screen from '../layout/Screen';
import {Button, ButtonTray} from '../UI/Button';
import CacheCardItem from '../gameplay/CacheCardItem';
@@ -11,7 +10,7 @@ import PlayerMapView from '../gameplay/PlayerMapView';
import useGameHook from '../../hooks/useGameHook';
import usePlayerGame from '../../hooks/usePlayerGame';
import {getSession, setSessionGroup, setSessionUser} from '../../hooks/SessionStore';
import {getFovCone, toHeading} from '../../utils/geoMath';
import {getFovCone} from '../../utils/geoMath';
const DEFAULT_REGION = {latitude: 51.5074, longitude: -0.1278, latitudeDelta: 0.05, longitudeDelta: 0.05};
@@ -87,18 +86,27 @@ const MapScreen = ({navigation}) => {
setPlayerLoading(false);
return;
}
const current = await Location.getCurrentPositionAsync({accuracy: Location.Accuracy.Balanced});
setUserLocation({latitude: current.coords.latitude, longitude: current.coords.longitude});
// Try cached position first for instant load, fall back to fresh fix
const last = await Location.getLastKnownPositionAsync();
if (last) {
setUserLocation({latitude: last.coords.latitude, longitude: last.coords.longitude});
} else {
const current = await Location.getCurrentPositionAsync({accuracy: Location.Accuracy.Low});
setUserLocation({latitude: current.coords.latitude, longitude: current.coords.longitude});
}
setPlayerLoading(false);
locationSub = await Location.watchPositionAsync(
{accuracy: Location.Accuracy.Balanced, distanceInterval: 1, timeInterval: 1000},
(next) => setUserLocation({latitude: next.coords.latitude, longitude: next.coords.longitude}),
);
// Track heading for all in-game users (admin + player)
Magnetometer.setUpdateInterval(500);
headingSub = Magnetometer.addListener((data) => setHeading(toHeading(data)));
setPlayerLoading(false);
// Use OS-fused heading from expo-location
headingSub = await Location.watchHeadingAsync((headingData) => {
const raw = headingData.trueHeading >= 0 ? headingData.trueHeading : headingData.magHeading;
setHeading(raw);
});
};
start();
@@ -110,7 +118,7 @@ const MapScreen = ({navigation}) => {
const handleJoinGame = async () => {
if (!gameCode.trim()) return;
const result = await joinPrivateGame({JoinCode: gameCode.trim(), Uid: session.currentUid});
const result = await joinPrivateGame({JoinCode: gameCode.trim().toUpperCase(), Uid: session.currentUid});
if (!result) return;
setSessionGroup(result.Gid, result.SGid);
setInGame(true);
@@ -196,13 +204,14 @@ const MapScreen = ({navigation}) => {
setEditingCacheId(null);
};
// Navigate to expanded map view
// Navigate to expanded map view, passing the live location so it opens centred on the user
const handleExpandMap = () => {
navigation.navigate('ExpandedMapScreen', {
isAdmin,
cacheRecords: JSON.stringify(cacheRecords),
heading: heading || 0,
claimDistance,
userLocation: userLocation || null,
});
};
@@ -306,7 +315,8 @@ const MapScreen = ({navigation}) => {
value={newClue}
onChangeText={setNewClue}
/>
<View style={styles.coordRow}> <View style={styles.coordField}>
<View style={styles.coordRow}>
<View style={styles.coordField}>
<Text style={styles.coordLabel}>Latitude</Text>
<TextInput
style={styles.formInput}
+50 -182
View File
@@ -1,5 +1,5 @@
import React, {useCallback, useEffect, useState} from 'react';
import {ActivityIndicator, Pressable, ScrollView, StyleSheet, Text, View} from 'react-native';
import {ActivityIndicator, ScrollView, StyleSheet, Text, View} from 'react-native';
import Screen from '../layout/Screen';
import Card from '../UI/Card';
import {Button} from '../UI/Button';
@@ -10,93 +10,59 @@ const PlayersScreen = () => {
// Initialisation ------------
const session = getSession();
const {getLobby, getTeams, getTeamMembers, getGroupMembers, removeMember, getUser, getCaches} = useGameHook();
const {getTeams, getTeamMembers, getGroupMembers, removeMember, getUser} = 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 (!session.currentGid) { setLoading(false); return; }
if (group && group.TeamsEnabled) {
const teamRows = await getTeams(session.currentGid);
setTeams(teamRows || []);
} else {
const allMembers = await getGroupMembers(session.currentGid);
const nonAdminMembers = (allMembers || []).filter((m) => !m.IsAcceptedAdmin);
const playerList = [];
for (const m of nonAdminMembers) {
const user = await getUser(m.Uid);
if (user) playerList.push({...user, membershipId: m.id});
// Get all non-admin players
const allMembers = await getGroupMembers(session.currentGid);
const nonAdminMembers = (allMembers || []).filter((m) => !m.IsAcceptedAdmin);
// Build team lookup: { Uid -> { teamCode, teamName, isLeader } }
const allTeams = await getTeams(session.currentGid);
const teamInfoByUid = {};
for (const team of (allTeams || [])) {
const tms = await getTeamMembers(team.Tid);
for (const tm of (tms || [])) {
teamInfoByUid[tm.Uid] = {
teamCode: team.JoinCode,
teamName: team.TeamName,
isLeader: Boolean(tm.IsLeader),
};
}
setPlayers(playerList);
}
// Build player list
const playerList = [];
for (const m of nonAdminMembers) {
const user = await getUser(m.Uid);
if (user) {
playerList.push({
...user,
membershipId: m.id,
team: teamInfoByUid[m.Uid] || null,
});
}
}
setPlayers(playerList);
setLoading(false);
}, [session.currentGid]);
useEffect(() => { loadData(); }, [loadData]);
const handleSelectTeam = async (team) => {
setSelectedTeam(team);
setDetailLoading(true);
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);
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({});
};
const handleRemovePlayer = async (membershipId) => {
await removeMember(membershipId);
await loadData();
};
const handleRemoveTeamMember = async (member) => {
// Find this user's subgroup membership to remove them from the game
const allMembers = await getGroupMembers(session.currentGid);
const membership = (allMembers || []).find((m) => m.Uid === member.Uid && !m.IsAcceptedAdmin);
if (membership) {
await removeMember(membership.id);
await handleSelectTeam(selectedTeam);
}
};
// View -----------------------
if (loading) {
@@ -115,101 +81,24 @@ const PlayersScreen = () => {
);
}
// 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>
<View style={styles.removeWrap}>
<Button
label="Remove"
onClick={() => handleRemoveTeamMember(m)}
styleButton={styles.removeButton}
styleLabel={styles.removeLabel}
/>
</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}>
<View style={styles.memberRow}>
<Text style={styles.playerName}>{player.username}</Text>
</View>
<View style={styles.removeWrap}>
<View style={styles.playerRow}>
<Text style={styles.playerName} numberOfLines={1}>{player.username}</Text>
<View style={styles.playerMeta}>
{player.team?.isLeader && (
<Text style={styles.leaderBadge}>(Team Leader)</Text>
)}
{player.team && (
<Text style={styles.teamCodeBadge}>{player.team.teamCode}</Text>
)}
</View>
<Button
label="Remove"
label="Kick"
onClick={() => handleRemovePlayer(player.membershipId)}
styleButton={styles.removeButton}
styleLabel={styles.removeLabel}
@@ -239,38 +128,17 @@ const styles = StyleSheet.create({
},
listSection: {flex: 1, paddingHorizontal: 15},
emptyText: {color: '#9ca3af', textAlign: 'center', marginTop: 30, fontSize: 14},
// Team cards
teamCardRow: {
playerRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
gap: 8,
},
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'},
// Remove button
removeWrap: {marginTop: 8},
removeButton: {backgroundColor: '#dc2626', borderColor: '#dc2626', minHeight: 36},
playerName: {fontSize: 15, fontWeight: '600', color: '#1f2937', flex: 1},
playerMeta: {flexDirection: 'row', alignItems: 'center', gap: 6},
leaderBadge: {fontSize: 11, color: '#2563eb', fontWeight: '700'},
teamCodeBadge: {fontSize: 12, fontWeight: '700', color: '#16a34a', letterSpacing: 1},
removeButton: {backgroundColor: '#dc2626', borderColor: '#dc2626', minHeight: 36, flex: 0, paddingHorizontal: 12},
removeLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13},
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;
+54 -22
View File
@@ -1,6 +1,7 @@
import React, {useEffect, useState} from 'react';
import {ActivityIndicator, ScrollView, StyleSheet, Text, TextInput, View} from 'react-native';
import Screen from '../layout/Screen';
import Card from '../UI/Card';
import {Button, ButtonTray} from '../UI/Button';
import useGameHook from '../../hooks/useGameHook';
import {getSession, setSessionTeam} from '../../hooks/SessionStore';
@@ -23,20 +24,22 @@ const TeamScreen = () => {
// Handlers -------------------
const loadTeamData = async (tid) => {
const t = await getTeam(tid);
setTeam(t);
const ms = await getTeamMembers(tid);
setMembers(ms || []);
const names = {};
for (const m of (ms || [])) {
const u = await getUser(m.Uid);
if (u) names[m.Uid] = u.username;
}
setMemberNames(names);
};
useEffect(() => {
const load = async () => {
if (activeTid) {
const t = await getTeam(activeTid);
setTeam(t);
const ms = await getTeamMembers(activeTid);
setMembers(ms || []);
const names = {};
for (const m of (ms || [])) {
const u = await getUser(m.Uid);
if (u) names[m.Uid] = u.username;
}
setMemberNames(names);
}
if (activeTid) await loadTeamData(activeTid);
setLoading(false);
};
load();
@@ -44,7 +47,7 @@ const TeamScreen = () => {
const handleJoinTeam = async () => {
if (!teamCode.trim()) return;
const result = await joinTeamByCode({JoinCode: teamCode.trim(), Uid: session.currentUid});
const result = await joinTeamByCode({JoinCode: teamCode.trim().toUpperCase(), Uid: session.currentUid});
if (!result) return;
setSessionTeam(result.Tid);
setActiveTid(result.Tid);
@@ -73,6 +76,15 @@ const TeamScreen = () => {
setMemberNames({});
};
const handleKickMember = async (member) => {
await leaveTeam(member.id);
await loadTeamData(activeTid);
};
// Check if current user is the team leader
const myMembership = members.find((m) => m.Uid === session.currentUid);
const isCurrentUserLeader = Boolean(myMembership?.IsLeader);
// View -----------------------
if (loading) {
@@ -91,6 +103,7 @@ const TeamScreen = () => {
<TextInput
style={styles.codeInput}
placeholder="Enter Team Code"
placeholderTextColor="#9ca3af"
value={teamCode}
onChangeText={setTeamCode}
autoCapitalize="characters"
@@ -127,9 +140,24 @@ const TeamScreen = () => {
</View>
<ScrollView style={styles.memberList}>
{members.map((m) => (
<View key={m.id} style={styles.memberItem}>
<Text style={styles.memberName}>{memberNames[m.Uid] || `Player ${m.Uid}`}</Text>
</View>
<Card key={m.id}>
<View style={styles.memberRow}>
<View style={styles.memberInfo}>
<Text style={styles.memberName}>
{memberNames[m.Uid] || `Player ${m.Uid}`}
</Text>
{m.IsLeader && <Text style={styles.leaderBadge}>(Team Leader)</Text>}
</View>
{isCurrentUserLeader && m.Uid !== session.currentUid && (
<Button
label="Kick"
onClick={() => handleKickMember(m)}
styleButton={styles.kickButton}
styleLabel={styles.kickLabel}
/>
)}
</View>
</Card>
))}
{members.length === 0 && (
<Text style={styles.emptyText}>No members yet.</Text>
@@ -178,14 +206,18 @@ const styles = StyleSheet.create({
teamCodeLabel: {color: '#d1d5db', fontSize: 15, fontWeight: '600'},
teamCodeValue: {color: '#ffffff', fontSize: 17, fontWeight: '700', letterSpacing: 2},
memberList: {flex: 1, paddingHorizontal: 15, paddingTop: 10},
memberItem: {
paddingVertical: 14,
borderBottomWidth: 1,
borderBottomColor: '#e5e7eb',
memberRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
memberName: {fontSize: 16, color: '#1f2937'},
memberInfo: {flex: 1, flexDirection: 'row', alignItems: 'center', gap: 6},
memberName: {fontSize: 16, color: '#1f2937', fontWeight: '600'},
leaderBadge: {fontSize: 12, color: '#2563eb', fontWeight: '700'},
kickButton: {backgroundColor: '#dc2626', borderColor: '#dc2626', minHeight: 36, flex: 0, paddingHorizontal: 14},
kickLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13},
emptyText: {color: '#9ca3af', textAlign: 'center', marginTop: 30, fontSize: 14},
leaveWrap: {padding: 15},
leaveWrap: {paddingHorizontal: 15, paddingVertical: 10},
leaveButton: {backgroundColor: '#dc2626', borderColor: '#dc2626'},
leaveLabel: {color: '#ffffff', fontWeight: '600'},
});
+45 -2
View File
@@ -1,4 +1,8 @@
// --- Session Store ---
// --- Session Store (with AsyncStorage persistence) ---
import AsyncStorage from '@react-native-async-storage/async-storage';
const STORAGE_KEY = '@mad_session';
const session = {
currentUid: null,
@@ -7,6 +11,7 @@ const session = {
currentTid: null,
isBusiness: null,
isAcceptedAdmin: false,
teamsEnabled: false,
};
// --- Handlers ---
@@ -18,6 +23,7 @@ const setSessionUser = (user) => {
session.currentTid = user?.TGid ?? null;
session.isBusiness = user?.isBusiness ?? null;
session.isAcceptedAdmin = Boolean(user?.IsAcceptedAdmin);
persistSession();
};
const setSessionGroup = (gid, sgid = null) => {
@@ -25,10 +31,17 @@ const setSessionGroup = (gid, sgid = null) => {
if (sgid !== null && sgid !== undefined) {
session.currentSGid = sgid;
}
persistSession();
};
const setSessionTeam = (tid) => {
session.currentTid = tid ?? null;
persistSession();
};
const setSessionTeamsEnabled = (val) => {
session.teamsEnabled = Boolean(val);
persistSession();
};
const getSession = () => ({ ...session });
@@ -40,6 +53,36 @@ const clearSession = () => {
session.currentTid = null;
session.isBusiness = null;
session.isAcceptedAdmin = false;
AsyncStorage.removeItem(STORAGE_KEY).catch(() => {});
};
export { setSessionUser, setSessionGroup, setSessionTeam, getSession, clearSession };
// Clear game data but keep user logged in
const clearGameSession = () => {
session.currentGid = null;
session.currentSGid = null;
session.currentTid = null;
session.isAcceptedAdmin = false;
session.teamsEnabled = false;
persistSession();
};
// --- Persistence helpers ---
const persistSession = () => {
AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(session)).catch(() => {});
};
const loadSession = async () => {
try {
const raw = await AsyncStorage.getItem(STORAGE_KEY);
if (raw) {
const saved = JSON.parse(raw);
Object.assign(session, saved);
}
} catch (_) {
// Ignore read errors — start fresh
}
return { ...session };
};
export { setSessionUser, setSessionGroup, setSessionTeam, setSessionTeamsEnabled, getSession, clearSession, clearGameSession, loadSession };
-21
View File
@@ -4,14 +4,12 @@ const useGameHook = () => {
// Initialisation ------------
const usersEndpoint = `${API_BASE_URL}/users`;
const gameTypesEndpoint = `${API_BASE_URL}/game-types`;
const groupsEndpoint = `${API_BASE_URL}/groups`;
const subgroupsEndpoint = `${API_BASE_URL}/subgroups`;
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`;
// Handlers -------------------
@@ -26,18 +24,7 @@ const useGameHook = () => {
return response.isSuccess ? response.result : null;
};
// Game types
const getGameTypes = async () => {
const response = await API.get(gameTypesEndpoint);
return response.isSuccess ? response.result : [];
};
// Groups
const getCreatedPrivateGame = async (userId) => {
const response = await API.get(`${groupsEndpoint}?CreatedByUid=${userId}`);
return response.isSuccess ? response.result : [];
};
const createPrivateGame = async (payload) => {
const response = await API.post(groupsEndpoint, payload);
return response.isSuccess ? response.result : null;
@@ -74,11 +61,6 @@ const useGameHook = () => {
return response.isSuccess;
};
const joinAsAdmin = async (payload) => {
const response = await API.post(adminWaitlistEndpoint, payload);
return response.isSuccess ? response.result : null;
};
// Teams
const getTeams = async (gid) => {
const response = await API.get(`${teamsEndpoint}?Gid=${gid}`);
@@ -152,14 +134,11 @@ const useGameHook = () => {
return {
getUser,
updateUser,
getGameTypes,
getCreatedPrivateGame,
createPrivateGame,
getLobby,
updateGroup,
getSubgroups,
joinPrivateGame,
joinAsAdmin,
getGroupMembers,
removeMember,
getTeams,