From d34bb650f41b2fc30a97d3bc9d5e43d49bfb6cf7 Mon Sep 17 00:00:00 2001 From: mudabbir-ahmad Date: Tue, 7 Apr 2026 14:38:18 +0100 Subject: [PATCH 1/6] Add ExpandedMapScreen and integrate it into navigation; enhance CacheCardItem and GameSettingsScreen with improved UI elements --- App.js | 3 + server/server.js | 2 + src/components/gameplay/CacheCardItem.js | 12 +- src/components/gameplay/ClaimTimerView.js | 3 +- src/components/gameplay/PlayerMapView.js | 82 +++++++---- src/components/layout/Screen.js | 1 + src/components/screens/ExpandedMapScreen.js | 117 +++++++++++++++ src/components/screens/GameSettingsScreen.js | 112 ++++++++------- src/components/screens/LoginScreen.js | 4 + src/components/screens/MapScreen.js | 141 +++++++++++++++---- src/components/screens/PlayersScreen.js | 53 +++++-- src/components/screens/RegisterScreen.js | 6 + src/components/screens/TeamScreen.js | 2 + src/hooks/useGameHook.js | 6 + src/utils/geoMath.js | 8 +- 15 files changed, 426 insertions(+), 126 deletions(-) create mode 100644 src/components/screens/ExpandedMapScreen.js diff --git a/App.js b/App.js index 15a3bbc..161871f 100644 --- a/App.js +++ b/App.js @@ -16,6 +16,7 @@ import AdminHubScreen from "./src/components/screens/AdminHubScreen"; import ManageSubgroupsScreen from "./src/components/screens/ManageSubgroupsScreen"; import GameSettingsScreen from "./src/components/screens/GameSettingsScreen"; import PlayersScreen from "./src/components/screens/PlayersScreen"; +import ExpandedMapScreen from "./src/components/screens/ExpandedMapScreen"; const RootStack = createNativeStackNavigator(); const AuthStackNav = createNativeStackNavigator(); @@ -47,6 +48,7 @@ const GameStack = () => ( + @@ -67,6 +69,7 @@ const AdminStack = () => ( + diff --git a/server/server.js b/server/server.js index 9f3df41..fe73fdc 100644 --- a/server/server.js +++ b/server/server.js @@ -560,6 +560,7 @@ addRoute('GET', '/game-data/:gid', (req, res, params) => { // Helper: convert storage cache to API response format const toCacheApi = (cache, gid) => ({ id: cache.CacheId, + name: cache.Name || '', coordinates: { latitude: cache.Latitude, longitude: cache.Longitude }, radius: cache.TriggerMeters, clue: cache.Title, @@ -572,6 +573,7 @@ const toCacheApi = (cache, gid) => ({ // Helper: convert API payload to storage cache format const toCacheStorage = (data, cacheId) => ({ CacheId: cacheId, + Name: data.name || data.Name || '', Title: data.clue || data.Title || '', Latitude: data.latitude != null ? data.latitude : (data.Latitude || 0), Longitude: data.longitude != null ? data.longitude : (data.Longitude || 0), diff --git a/src/components/gameplay/CacheCardItem.js b/src/components/gameplay/CacheCardItem.js index 7838843..f5c9341 100644 --- a/src/components/gameplay/CacheCardItem.js +++ b/src/components/gameplay/CacheCardItem.js @@ -8,6 +8,7 @@ const CacheCardItem = ({cache, isAdmin, onEdit, onDelete, onSelect}) => { const isClaimed = Boolean(cache.ClaimedByUid); const statusText = isClaimed ? 'Claimed' : 'Available'; const statusColor = isClaimed ? '#9ca3af' : '#16a34a'; + const displayName = cache.name || cache.clue || 'Unnamed Cache'; // State ---------------------- // Handlers ------------------- @@ -16,7 +17,12 @@ const CacheCardItem = ({cache, isAdmin, onEdit, onDelete, onSelect}) => { return ( - {cache.clue} + + {displayName} + {cache.name ? ( + {cache.clue} + ) : null} + {statusText} @@ -55,7 +61,9 @@ const styles = StyleSheet.create({ alignItems: 'center', marginBottom: 8, }, - clue: {fontSize: 15, fontWeight: '600', color: '#1f2937', flex: 1, marginRight: 8}, + clue: {fontSize: 13, color: '#6b7280', marginTop: 2}, + nameWrap: {flex: 1, marginRight: 8}, + name: {fontSize: 15, fontWeight: '600', color: '#1f2937'}, status: {fontSize: 13, fontWeight: '600'}, actions: {flexDirection: 'row', gap: 8}, editBtn: { diff --git a/src/components/gameplay/ClaimTimerView.js b/src/components/gameplay/ClaimTimerView.js index 0b46031..68076a8 100644 --- a/src/components/gameplay/ClaimTimerView.js +++ b/src/components/gameplay/ClaimTimerView.js @@ -36,13 +36,12 @@ const ClaimTimerView = ({ cache, onClaimSuccess, isClaiming }) => { const styles = StyleSheet.create({ overlay: { - position: 'absolute', - bottom: 24, alignSelf: 'center', backgroundColor: 'rgba(0,0,0,0.75)', paddingHorizontal: 16, paddingVertical: 12, borderRadius: 10, + marginTop: 8, }, text: { color: '#ffffff', diff --git a/src/components/gameplay/PlayerMapView.js b/src/components/gameplay/PlayerMapView.js index b96bc88..b4f3dfb 100644 --- a/src/components/gameplay/PlayerMapView.js +++ b/src/components/gameplay/PlayerMapView.js @@ -1,8 +1,8 @@ import React from 'react'; -import MapView, {Circle} from 'react-native-maps'; -import ClaimTimerView from './ClaimTimerView'; +import {StyleSheet, View} from 'react-native'; +import MapView, {Circle, Marker} from 'react-native-maps'; -const PlayerMapView = ({ userLocation, visibleCache, isClaiming, onClaimSuccess }) => { +const PlayerMapView = ({ userLocation, visibleCache, heading }) => { // Initialisation ------------- // State ---------------------- // Handlers ------------------- @@ -11,34 +11,56 @@ const PlayerMapView = ({ userLocation, visibleCache, isClaiming, onClaimSuccess if (!userLocation) return null; return ( - <> - - {visibleCache ? ( - - ) : null} - - - - + + {visibleCache ? ( + + ) : null} + {heading !== null && heading !== undefined && ( + + + + + + )} + ); }; -export default PlayerMapView; +const styles = StyleSheet.create({ + coneWrap: {width: 22, height: 24, alignItems: 'center'}, + cone: { + width: 0, + height: 0, + borderTopWidth: 24, + borderLeftWidth: 11, + borderRightWidth: 11, + borderTopColor: 'rgba(37,99,235,0.70)', + borderLeftColor: 'transparent', + borderRightColor: 'transparent', + }, +}); +export default PlayerMapView; diff --git a/src/components/layout/Screen.js b/src/components/layout/Screen.js index 920d2d4..d5fd5f0 100644 --- a/src/components/layout/Screen.js +++ b/src/components/layout/Screen.js @@ -26,6 +26,7 @@ const Screen = ({ children, style, showBack = false }) => { ManageSubgroupsScreen: "Manage Subgroups", GameSettingsScreen: "Game Settings", PlayersScreen: "Players", + ExpandedMapScreen: "Map View", }; // State ---------------------- diff --git a/src/components/screens/ExpandedMapScreen.js b/src/components/screens/ExpandedMapScreen.js new file mode 100644 index 0000000..148e829 --- /dev/null +++ b/src/components/screens/ExpandedMapScreen.js @@ -0,0 +1,117 @@ +import React, {useEffect, useState} from 'react'; +import {StyleSheet, View} from 'react-native'; +import MapView, {Circle, Marker} from 'react-native-maps'; +import * as Location from 'expo-location'; +import {Magnetometer} from 'expo-sensors'; +import Screen from '../layout/Screen'; +import {toHeading} 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} = route.params || {}; + const caches = cacheStr ? JSON.parse(cacheStr) : []; + +// State ---------------------- + + const [userLocation, setUserLocation] = useState(null); + const [heading, setHeading] = useState(null); + +// Handlers ------------------- + + useEffect(() => { + let locationSub; + let headingSub; + + const start = async () => { + 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}); + + 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))); + }; + + start(); + return () => { + if (locationSub) locationSub.remove(); + if (headingSub) headingSub.remove(); + }; + }, []); + + const region = userLocation + ? {...userLocation, latitudeDelta: 0.01, longitudeDelta: 0.01} + : DEFAULT_REGION; + +// View ----------------------- + + return ( + + + + {/* Only admins see cache pin locations */} + {isAdmin && caches.map((cache) => ( + + + + + ))} + {/* Heading cone for both admin and player */} + {userLocation && heading !== null && ( + + + + + + )} + + + + ); +}; + +const styles = StyleSheet.create({ + container: {padding: 0}, + mapWrap: {flex: 1}, + coneWrap: {width: 22, height: 24, alignItems: 'center'}, + cone: { + width: 0, + height: 0, + borderTopWidth: 24, + borderLeftWidth: 11, + borderRightWidth: 11, + borderTopColor: 'rgba(37,99,235,0.70)', + borderLeftColor: 'transparent', + borderRightColor: 'transparent', + }, +}); + +export default ExpandedMapScreen; diff --git a/src/components/screens/GameSettingsScreen.js b/src/components/screens/GameSettingsScreen.js index b5443b6..d5f2902 100644 --- a/src/components/screens/GameSettingsScreen.js +++ b/src/components/screens/GameSettingsScreen.js @@ -1,5 +1,5 @@ import React, {useEffect, useState} from 'react'; -import {ActivityIndicator, StyleSheet, Switch, Text, TextInput, View} from 'react-native'; +import {ActivityIndicator, ScrollView, StyleSheet, Switch, Text, TextInput, View} from 'react-native'; import Screen from '../layout/Screen'; import {Button, ButtonTray} from '../UI/Button'; import useGameHook from '../../hooks/useGameHook'; @@ -76,65 +76,72 @@ const GameSettingsScreen = () => { return ( - Game Configuration + + Game Configuration - Game Name - - - - Teams Enabled - Game Name + - - {isBusiness && ( - <> - Max Member Subgroups - + Teams Enabled + - - )} + - - Admin Join Code - {adminJoinCode || '—'} - + {isBusiness && ( + <> + Max Member Subgroups + + + )} - - Player Join Code - {memberJoinCode || '—'} - + + Admin Join Code + {adminJoinCode || '—'} + - -