mirror of
https://github.com/mudabbir-ahmad/MAD-Treasure-Hunt.git
synced 2026-10-08 03:00:20 +00:00
Add ExpandedMapScreen and integrate it into navigation; enhance CacheCardItem and GameSettingsScreen with improved UI elements
This commit is contained in:
1 parent
60cf6baad2
commit
d34bb650f4
15 files changed
+426
-126
No files matched your search
@@ -16,6 +16,7 @@ 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 GameSettingsScreen from "./src/components/screens/GameSettingsScreen";
|
||||||
import PlayersScreen from "./src/components/screens/PlayersScreen";
|
import PlayersScreen from "./src/components/screens/PlayersScreen";
|
||||||
|
import ExpandedMapScreen from "./src/components/screens/ExpandedMapScreen";
|
||||||
|
|
||||||
const RootStack = createNativeStackNavigator();
|
const RootStack = createNativeStackNavigator();
|
||||||
const AuthStackNav = createNativeStackNavigator();
|
const AuthStackNav = createNativeStackNavigator();
|
||||||
@@ -47,6 +48,7 @@ const GameStack = () => (
|
|||||||
<GameStackNav.Screen name="LeaderboardScreen" component={LeaderboardScreen}/>
|
<GameStackNav.Screen name="LeaderboardScreen" component={LeaderboardScreen}/>
|
||||||
<GameStackNav.Screen name="GameSettingsScreen" component={GameSettingsScreen}/>
|
<GameStackNav.Screen name="GameSettingsScreen" component={GameSettingsScreen}/>
|
||||||
<GameStackNav.Screen name="PlayersScreen" component={PlayersScreen}/>
|
<GameStackNav.Screen name="PlayersScreen" component={PlayersScreen}/>
|
||||||
|
<GameStackNav.Screen name="ExpandedMapScreen" component={ExpandedMapScreen}/>
|
||||||
<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}/>
|
||||||
@@ -67,6 +69,7 @@ const AdminStack = () => (
|
|||||||
<AdminStackNav.Screen name="MapScreen" component={MapScreen}/>
|
<AdminStackNav.Screen name="MapScreen" component={MapScreen}/>
|
||||||
<AdminStackNav.Screen name="LeaderboardScreen" component={LeaderboardScreen}/>
|
<AdminStackNav.Screen name="LeaderboardScreen" component={LeaderboardScreen}/>
|
||||||
<AdminStackNav.Screen name="PlayersScreen" component={PlayersScreen}/>
|
<AdminStackNav.Screen name="PlayersScreen" component={PlayersScreen}/>
|
||||||
|
<AdminStackNav.Screen name="ExpandedMapScreen" component={ExpandedMapScreen}/>
|
||||||
<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="TeamScreen" component={TeamScreen}/>
|
<AdminStackNav.Screen name="TeamScreen" component={TeamScreen}/>
|
||||||
|
|||||||
@@ -560,6 +560,7 @@ addRoute('GET', '/game-data/:gid', (req, res, params) => {
|
|||||||
// Helper: convert storage cache to API response format
|
// Helper: convert storage cache to API response format
|
||||||
const toCacheApi = (cache, gid) => ({
|
const toCacheApi = (cache, gid) => ({
|
||||||
id: cache.CacheId,
|
id: cache.CacheId,
|
||||||
|
name: cache.Name || '',
|
||||||
coordinates: { latitude: cache.Latitude, longitude: cache.Longitude },
|
coordinates: { latitude: cache.Latitude, longitude: cache.Longitude },
|
||||||
radius: cache.TriggerMeters,
|
radius: cache.TriggerMeters,
|
||||||
clue: cache.Title,
|
clue: cache.Title,
|
||||||
@@ -572,6 +573,7 @@ const toCacheApi = (cache, gid) => ({
|
|||||||
// Helper: convert API payload to storage cache format
|
// Helper: convert API payload to storage cache format
|
||||||
const toCacheStorage = (data, cacheId) => ({
|
const toCacheStorage = (data, cacheId) => ({
|
||||||
CacheId: cacheId,
|
CacheId: cacheId,
|
||||||
|
Name: data.name || data.Name || '',
|
||||||
Title: data.clue || data.Title || '',
|
Title: data.clue || data.Title || '',
|
||||||
Latitude: data.latitude != null ? data.latitude : (data.Latitude || 0),
|
Latitude: data.latitude != null ? data.latitude : (data.Latitude || 0),
|
||||||
Longitude: data.longitude != null ? data.longitude : (data.Longitude || 0),
|
Longitude: data.longitude != null ? data.longitude : (data.Longitude || 0),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const CacheCardItem = ({cache, isAdmin, onEdit, onDelete, onSelect}) => {
|
|||||||
const isClaimed = Boolean(cache.ClaimedByUid);
|
const isClaimed = Boolean(cache.ClaimedByUid);
|
||||||
const statusText = isClaimed ? 'Claimed' : 'Available';
|
const statusText = isClaimed ? 'Claimed' : 'Available';
|
||||||
const statusColor = isClaimed ? '#9ca3af' : '#16a34a';
|
const statusColor = isClaimed ? '#9ca3af' : '#16a34a';
|
||||||
|
const displayName = cache.name || cache.clue || 'Unnamed Cache';
|
||||||
|
|
||||||
// State ----------------------
|
// State ----------------------
|
||||||
// Handlers -------------------
|
// Handlers -------------------
|
||||||
@@ -16,7 +17,12 @@ const CacheCardItem = ({cache, isAdmin, onEdit, onDelete, onSelect}) => {
|
|||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<View style={styles.topRow}>
|
<View style={styles.topRow}>
|
||||||
<Text style={styles.clue} numberOfLines={1}>{cache.clue}</Text>
|
<View style={styles.nameWrap}>
|
||||||
|
<Text style={styles.name} numberOfLines={1}>{displayName}</Text>
|
||||||
|
{cache.name ? (
|
||||||
|
<Text style={styles.clue} numberOfLines={1}>{cache.clue}</Text>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
<Text style={[styles.status, {color: statusColor}]}>{statusText}</Text>
|
<Text style={[styles.status, {color: statusColor}]}>{statusText}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.actions}>
|
<View style={styles.actions}>
|
||||||
@@ -55,7 +61,9 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: 8,
|
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'},
|
status: {fontSize: 13, fontWeight: '600'},
|
||||||
actions: {flexDirection: 'row', gap: 8},
|
actions: {flexDirection: 'row', gap: 8},
|
||||||
editBtn: {
|
editBtn: {
|
||||||
|
|||||||
@@ -36,13 +36,12 @@ const ClaimTimerView = ({ cache, onClaimSuccess, isClaiming }) => {
|
|||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
overlay: {
|
overlay: {
|
||||||
position: 'absolute',
|
|
||||||
bottom: 24,
|
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
backgroundColor: 'rgba(0,0,0,0.75)',
|
backgroundColor: 'rgba(0,0,0,0.75)',
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
|
marginTop: 8,
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
color: '#ffffff',
|
color: '#ffffff',
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import MapView, {Circle} from 'react-native-maps';
|
import {StyleSheet, View} from 'react-native';
|
||||||
import ClaimTimerView from './ClaimTimerView';
|
import MapView, {Circle, Marker} from 'react-native-maps';
|
||||||
|
|
||||||
const PlayerMapView = ({ userLocation, visibleCache, isClaiming, onClaimSuccess }) => {
|
const PlayerMapView = ({ userLocation, visibleCache, heading }) => {
|
||||||
// Initialisation -------------
|
// Initialisation -------------
|
||||||
// State ----------------------
|
// State ----------------------
|
||||||
// Handlers -------------------
|
// Handlers -------------------
|
||||||
@@ -11,34 +11,56 @@ const PlayerMapView = ({ userLocation, visibleCache, isClaiming, onClaimSuccess
|
|||||||
if (!userLocation) return null;
|
if (!userLocation) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<MapView
|
||||||
<MapView
|
style={{flex: 1}}
|
||||||
style={{flex: 1}}
|
region={{
|
||||||
initialRegion={{
|
...userLocation,
|
||||||
...userLocation,
|
latitudeDelta: 0.01,
|
||||||
latitudeDelta: 0.01,
|
longitudeDelta: 0.01,
|
||||||
longitudeDelta: 0.01,
|
}}
|
||||||
}}
|
scrollEnabled={false}
|
||||||
showsUserLocation
|
zoomEnabled={true}
|
||||||
>
|
rotateEnabled={false}
|
||||||
{visibleCache ? (
|
pitchEnabled={false}
|
||||||
<Circle
|
showsUserLocation
|
||||||
center={visibleCache.coordinates}
|
>
|
||||||
radius={visibleCache.radius}
|
{visibleCache ? (
|
||||||
fillColor="rgba(250, 204, 21, 0.20)"
|
<Circle
|
||||||
strokeColor="rgba(250, 204, 21, 0.90)"
|
center={visibleCache.coordinates}
|
||||||
/>
|
radius={visibleCache.radius}
|
||||||
) : null}
|
fillColor="rgba(250, 204, 21, 0.20)"
|
||||||
</MapView>
|
strokeColor="rgba(250, 204, 21, 0.90)"
|
||||||
|
/>
|
||||||
<ClaimTimerView
|
) : null}
|
||||||
cache={visibleCache}
|
{heading !== null && heading !== undefined && (
|
||||||
isClaiming={isClaiming}
|
<Marker
|
||||||
onClaimSuccess={onClaimSuccess}
|
coordinate={userLocation}
|
||||||
/>
|
flat={true}
|
||||||
</>
|
rotation={heading}
|
||||||
|
anchor={{x: 0.5, y: 1.0}}
|
||||||
|
tracksViewChanges={false}
|
||||||
|
>
|
||||||
|
<View style={styles.coneWrap}>
|
||||||
|
<View style={styles.cone}/>
|
||||||
|
</View>
|
||||||
|
</Marker>
|
||||||
|
)}
|
||||||
|
</MapView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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;
|
||||||
@@ -26,6 +26,7 @@ const Screen = ({ children, style, showBack = false }) => {
|
|||||||
ManageSubgroupsScreen: "Manage Subgroups",
|
ManageSubgroupsScreen: "Manage Subgroups",
|
||||||
GameSettingsScreen: "Game Settings",
|
GameSettingsScreen: "Game Settings",
|
||||||
PlayersScreen: "Players",
|
PlayersScreen: "Players",
|
||||||
|
ExpandedMapScreen: "Map View",
|
||||||
};
|
};
|
||||||
|
|
||||||
// State ----------------------
|
// State ----------------------
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<Screen showBack={true} style={styles.container}>
|
||||||
|
<View style={styles.mapWrap}>
|
||||||
|
<MapView
|
||||||
|
style={{flex: 1}}
|
||||||
|
initialRegion={region}
|
||||||
|
showsUserLocation
|
||||||
|
>
|
||||||
|
{/* Only admins see cache pin locations */}
|
||||||
|
{isAdmin && caches.map((cache) => (
|
||||||
|
<React.Fragment key={cache.id}>
|
||||||
|
<Marker
|
||||||
|
coordinate={cache.coordinates}
|
||||||
|
title={cache.name || cache.clue}
|
||||||
|
pinColor="#2563eb"
|
||||||
|
/>
|
||||||
|
<Circle
|
||||||
|
center={cache.coordinates}
|
||||||
|
radius={cache.radius}
|
||||||
|
fillColor="rgba(59,130,246,0.15)"
|
||||||
|
strokeColor="rgba(59,130,246,0.85)"
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
{/* Heading cone for both admin and player */}
|
||||||
|
{userLocation && heading !== null && (
|
||||||
|
<Marker
|
||||||
|
coordinate={userLocation}
|
||||||
|
flat={true}
|
||||||
|
rotation={heading}
|
||||||
|
anchor={{x: 0.5, y: 1.0}}
|
||||||
|
tracksViewChanges={false}
|
||||||
|
>
|
||||||
|
<View style={styles.coneWrap}>
|
||||||
|
<View style={styles.cone}/>
|
||||||
|
</View>
|
||||||
|
</Marker>
|
||||||
|
)}
|
||||||
|
</MapView>
|
||||||
|
</View>
|
||||||
|
</Screen>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, {useEffect, useState} from 'react';
|
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 Screen from '../layout/Screen';
|
||||||
import {Button, ButtonTray} from '../UI/Button';
|
import {Button, ButtonTray} from '../UI/Button';
|
||||||
import useGameHook from '../../hooks/useGameHook';
|
import useGameHook from '../../hooks/useGameHook';
|
||||||
@@ -76,65 +76,72 @@ const GameSettingsScreen = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Screen>
|
<Screen>
|
||||||
<Text style={styles.sectionTitle}>Game Configuration</Text>
|
<ScrollView contentContainerStyle={styles.formContainer} showsVerticalScrollIndicator={false}>
|
||||||
|
<Text style={styles.sectionTitle}>Game Configuration</Text>
|
||||||
|
|
||||||
<Text style={styles.label}>Game Name</Text>
|
<Text style={styles.label}>Game Name</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
value={groupName}
|
value={groupName}
|
||||||
onChangeText={setGroupName}
|
onChangeText={setGroupName}
|
||||||
placeholder="Enter game name"
|
placeholder="Enter game name"
|
||||||
/>
|
placeholderTextColor="#9ca3af"
|
||||||
|
|
||||||
<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 && (
|
<View style={styles.toggleRow}>
|
||||||
<>
|
<Text style={styles.toggleLabel}>Teams Enabled</Text>
|
||||||
<Text style={styles.label}>Max Member Subgroups</Text>
|
<Switch
|
||||||
<TextInput
|
value={teamsEnabled}
|
||||||
style={styles.input}
|
onValueChange={setTeamsEnabled}
|
||||||
value={maxSubgroups}
|
trackColor={{false: '#d1d5db', true: '#93c5fd'}}
|
||||||
onChangeText={setMaxSubgroups}
|
thumbColor={teamsEnabled ? '#2563eb' : '#9ca3af'}
|
||||||
keyboardType="numeric"
|
|
||||||
placeholder="1"
|
|
||||||
/>
|
/>
|
||||||
</>
|
</View>
|
||||||
)}
|
|
||||||
|
|
||||||
<View style={styles.codeSection}>
|
{isBusiness && (
|
||||||
<Text style={styles.codeLabel}>Admin Join Code</Text>
|
<>
|
||||||
<Text style={styles.codeValue}>{adminJoinCode || '—'}</Text>
|
<Text style={styles.label}>Max Member Subgroups</Text>
|
||||||
</View>
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
value={maxSubgroups}
|
||||||
|
onChangeText={setMaxSubgroups}
|
||||||
|
keyboardType="numeric"
|
||||||
|
placeholder="1"
|
||||||
|
placeholderTextColor="#9ca3af"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<View style={styles.codeSection}>
|
<View style={styles.codeSection}>
|
||||||
<Text style={styles.codeLabel}>Player Join Code</Text>
|
<Text style={styles.codeLabel}>Admin Join Code</Text>
|
||||||
<Text style={styles.codeValue}>{memberJoinCode || '—'}</Text>
|
<Text style={styles.codeValue}>{adminJoinCode || '—'}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<ButtonTray>
|
<View style={styles.codeSection}>
|
||||||
<Button
|
<Text style={styles.codeLabel}>Player Join Code</Text>
|
||||||
label={saving ? 'Saving...' : 'Save Settings'}
|
<Text style={styles.codeValue}>{memberJoinCode || '—'}</Text>
|
||||||
onClick={handleSave}
|
</View>
|
||||||
styleButton={styles.saveButton}
|
|
||||||
styleLabel={styles.saveLabel}
|
<View style={styles.saveWrap}>
|
||||||
/>
|
<ButtonTray>
|
||||||
</ButtonTray>
|
<Button
|
||||||
|
label={saving ? 'Saving...' : 'Save Settings'}
|
||||||
|
onClick={handleSave}
|
||||||
|
styleButton={styles.saveButton}
|
||||||
|
styleLabel={styles.saveLabel}
|
||||||
|
/>
|
||||||
|
</ButtonTray>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
</Screen>
|
</Screen>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
center: {justifyContent: 'center', alignItems: 'center'},
|
center: {justifyContent: 'center', alignItems: 'center'},
|
||||||
sectionTitle: {fontSize: 20, fontWeight: '700', color: '#1f2937', marginBottom: 15},
|
formContainer: {paddingBottom: 20},
|
||||||
label: {fontSize: 14, fontWeight: '600', color: '#374151', marginBottom: 4, marginTop: 10},
|
sectionTitle: {fontSize: 20, fontWeight: '700', color: '#1f2937', marginBottom: 16},
|
||||||
|
label: {fontSize: 14, fontWeight: '600', color: '#374151', marginBottom: 6, marginTop: 14},
|
||||||
body: {color: '#4b5563', fontSize: 15},
|
body: {color: '#4b5563', fontSize: 15},
|
||||||
input: {
|
input: {
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
@@ -143,29 +150,32 @@ const styles = StyleSheet.create({
|
|||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
paddingVertical: 10,
|
paddingVertical: 10,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
color: '#1f2937',
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: '#ffffff',
|
||||||
},
|
},
|
||||||
toggleRow: {
|
toggleRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginTop: 10,
|
marginTop: 16,
|
||||||
paddingVertical: 8,
|
paddingVertical: 10,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: '#e5e7eb',
|
borderBottomColor: '#e5e7eb',
|
||||||
},
|
},
|
||||||
|
toggleLabel: {fontSize: 14, fontWeight: '600', color: '#374151'},
|
||||||
codeSection: {
|
codeSection: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginTop: 14,
|
marginTop: 16,
|
||||||
paddingVertical: 10,
|
paddingVertical: 12,
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
backgroundColor: '#f3f4f6',
|
backgroundColor: '#f3f4f6',
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
},
|
},
|
||||||
codeLabel: {fontSize: 14, fontWeight: '600', color: '#374151'},
|
codeLabel: {fontSize: 14, fontWeight: '600', color: '#374151'},
|
||||||
codeValue: {fontSize: 16, fontWeight: '700', color: '#2563eb', letterSpacing: 2},
|
codeValue: {fontSize: 16, fontWeight: '700', color: '#2563eb', letterSpacing: 2},
|
||||||
|
saveWrap: {marginTop: 24},
|
||||||
saveButton: {backgroundColor: '#16a34a', borderColor: '#16a34a'},
|
saveButton: {backgroundColor: '#16a34a', borderColor: '#16a34a'},
|
||||||
saveLabel: {color: '#ffffff', fontWeight: '600'},
|
saveLabel: {color: '#ffffff', fontWeight: '600'},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ const LoginScreen = ({ navigation }) => {
|
|||||||
<TextInput
|
<TextInput
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
placeholder="Email"
|
placeholder="Email"
|
||||||
|
placeholderTextColor="#9ca3af"
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
keyboardType="email-address"
|
keyboardType="email-address"
|
||||||
value={email}
|
value={email}
|
||||||
@@ -48,6 +49,7 @@ const LoginScreen = ({ navigation }) => {
|
|||||||
<TextInput
|
<TextInput
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
|
placeholderTextColor="#9ca3af"
|
||||||
secureTextEntry
|
secureTextEntry
|
||||||
value={password}
|
value={password}
|
||||||
onChangeText={setPassword}
|
onChangeText={setPassword}
|
||||||
@@ -93,6 +95,8 @@ const styles = StyleSheet.create({
|
|||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
paddingVertical: 10,
|
paddingVertical: 10,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
color: '#1f2937',
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
},
|
},
|
||||||
errorText: {
|
errorText: {
|
||||||
color: '#dc2626',
|
color: '#dc2626',
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
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, Pressable, ScrollView, StyleSheet, Text, TextInput, View} from 'react-native';
|
||||||
import MapView, {Circle, Marker} from 'react-native-maps';
|
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 CacheCardItem from '../gameplay/CacheCardItem';
|
||||||
|
import ClaimTimerView from '../gameplay/ClaimTimerView';
|
||||||
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';
|
||||||
|
import {toHeading} from '../../utils/geoMath';
|
||||||
|
|
||||||
const DEFAULT_REGION = {latitude: 51.5074, longitude: -0.1278, latitudeDelta: 0.05, longitudeDelta: 0.05};
|
const DEFAULT_REGION = {latitude: 51.5074, longitude: -0.1278, latitudeDelta: 0.05, longitudeDelta: 0.05};
|
||||||
|
|
||||||
const toHeading = ({x, y}) => {
|
|
||||||
const angle = Math.atan2(y, x) * (180 / Math.PI);
|
|
||||||
return angle < 0 ? angle + 360 : angle;
|
|
||||||
};
|
|
||||||
|
|
||||||
const MapScreen = ({navigation}) => {
|
const MapScreen = ({navigation}) => {
|
||||||
// Initialisation ------------
|
// Initialisation ------------
|
||||||
@@ -40,6 +38,7 @@ const MapScreen = ({navigation}) => {
|
|||||||
const [isCreating, setIsCreating] = useState(false);
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
const [editingCacheId, setEditingCacheId] = useState(null);
|
const [editingCacheId, setEditingCacheId] = useState(null);
|
||||||
const [newCoord, setNewCoord] = useState(null);
|
const [newCoord, setNewCoord] = useState(null);
|
||||||
|
const [newName, setNewName] = useState('');
|
||||||
const [newClue, setNewClue] = useState('');
|
const [newClue, setNewClue] = useState('');
|
||||||
const [newRadius, setNewRadius] = useState('20');
|
const [newRadius, setNewRadius] = useState('20');
|
||||||
|
|
||||||
@@ -66,8 +65,7 @@ const MapScreen = ({navigation}) => {
|
|||||||
setCacheRecords([]);
|
setCacheRecords([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const sgFilter = isAdmin ? null : session.currentSGid;
|
const rows = await getCaches(session.currentGid, null);
|
||||||
const rows = await getCaches(session.currentGid, sgFilter);
|
|
||||||
setCacheRecords(rows || []);
|
setCacheRecords(rows || []);
|
||||||
}, [inGame, isAdmin]);
|
}, [inGame, isAdmin]);
|
||||||
|
|
||||||
@@ -94,10 +92,9 @@ 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) {
|
// Track heading for all in-game users (admin + player)
|
||||||
Magnetometer.setUpdateInterval(500);
|
Magnetometer.setUpdateInterval(500);
|
||||||
headingSub = Magnetometer.addListener((data) => setHeading(toHeading(data)));
|
headingSub = Magnetometer.addListener((data) => setHeading(toHeading(data)));
|
||||||
}
|
|
||||||
setPlayerLoading(false);
|
setPlayerLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -152,6 +149,7 @@ const MapScreen = ({navigation}) => {
|
|||||||
const fallback = userLocation || {latitude: mapRegion.latitude, longitude: mapRegion.longitude};
|
const fallback = userLocation || {latitude: mapRegion.latitude, longitude: mapRegion.longitude};
|
||||||
setEditingCacheId(null);
|
setEditingCacheId(null);
|
||||||
setNewCoord(fallback);
|
setNewCoord(fallback);
|
||||||
|
setNewName('');
|
||||||
setNewClue('');
|
setNewClue('');
|
||||||
setNewRadius('20');
|
setNewRadius('20');
|
||||||
setIsCreating(true);
|
setIsCreating(true);
|
||||||
@@ -161,6 +159,7 @@ const MapScreen = ({navigation}) => {
|
|||||||
const handleEditCache = (cache) => {
|
const handleEditCache = (cache) => {
|
||||||
setEditingCacheId(cache.id);
|
setEditingCacheId(cache.id);
|
||||||
setNewCoord(cache.coordinates);
|
setNewCoord(cache.coordinates);
|
||||||
|
setNewName(cache.name || '');
|
||||||
setNewClue(cache.clue || '');
|
setNewClue(cache.clue || '');
|
||||||
setNewRadius(String(cache.radius || 20));
|
setNewRadius(String(cache.radius || 20));
|
||||||
setIsCreating(true);
|
setIsCreating(true);
|
||||||
@@ -178,6 +177,7 @@ const MapScreen = ({navigation}) => {
|
|||||||
if (!newCoord || !newClue.trim() || !session.currentGid) return;
|
if (!newCoord || !newClue.trim() || !session.currentGid) return;
|
||||||
const payload = {
|
const payload = {
|
||||||
gid: session.currentGid,
|
gid: session.currentGid,
|
||||||
|
name: newName.trim(),
|
||||||
latitude: newCoord.latitude,
|
latitude: newCoord.latitude,
|
||||||
longitude: newCoord.longitude,
|
longitude: newCoord.longitude,
|
||||||
radius: parseInt(newRadius) || 20,
|
radius: parseInt(newRadius) || 20,
|
||||||
@@ -196,7 +196,16 @@ const MapScreen = ({navigation}) => {
|
|||||||
setEditingCacheId(null);
|
setEditingCacheId(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Player — select cache (scroll to / highlight)
|
// Navigate to expanded map view
|
||||||
|
const handleExpandMap = () => {
|
||||||
|
navigation.navigate('ExpandedMapScreen', {
|
||||||
|
isAdmin,
|
||||||
|
cacheRecords: JSON.stringify(cacheRecords),
|
||||||
|
heading: heading || 0,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Player — select cache
|
||||||
const handleSelectCache = (cache) => {
|
const handleSelectCache = (cache) => {
|
||||||
// Future: scroll map to cache location
|
// Future: scroll map to cache location
|
||||||
};
|
};
|
||||||
@@ -211,6 +220,7 @@ const MapScreen = ({navigation}) => {
|
|||||||
<TextInput
|
<TextInput
|
||||||
style={styles.codeInput}
|
style={styles.codeInput}
|
||||||
placeholder="Enter Game Code"
|
placeholder="Enter Game Code"
|
||||||
|
placeholderTextColor="#9ca3af"
|
||||||
value={gameCode}
|
value={gameCode}
|
||||||
onChangeText={setGameCode}
|
onChangeText={setGameCode}
|
||||||
autoCapitalize="characters"
|
autoCapitalize="characters"
|
||||||
@@ -283,7 +293,15 @@ const MapScreen = ({navigation}) => {
|
|||||||
<ScrollView style={styles.formSection}>
|
<ScrollView style={styles.formSection}>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={styles.formInput}
|
style={styles.formInput}
|
||||||
placeholder="Cache's Clue"
|
placeholder="Cache Name"
|
||||||
|
placeholderTextColor="#9ca3af"
|
||||||
|
value={newName}
|
||||||
|
onChangeText={setNewName}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
style={styles.formInput}
|
||||||
|
placeholder="Cache Clue"
|
||||||
|
placeholderTextColor="#9ca3af"
|
||||||
value={newClue}
|
value={newClue}
|
||||||
onChangeText={setNewClue}
|
onChangeText={setNewClue}
|
||||||
/>
|
/>
|
||||||
@@ -293,6 +311,7 @@ const MapScreen = ({navigation}) => {
|
|||||||
<TextInput
|
<TextInput
|
||||||
style={styles.formInput}
|
style={styles.formInput}
|
||||||
placeholder="Latitude"
|
placeholder="Latitude"
|
||||||
|
placeholderTextColor="#9ca3af"
|
||||||
value={newCoord ? String(newCoord.latitude.toFixed(6)) : ''}
|
value={newCoord ? String(newCoord.latitude.toFixed(6)) : ''}
|
||||||
editable={false}
|
editable={false}
|
||||||
/>
|
/>
|
||||||
@@ -302,6 +321,7 @@ const MapScreen = ({navigation}) => {
|
|||||||
<TextInput
|
<TextInput
|
||||||
style={styles.formInput}
|
style={styles.formInput}
|
||||||
placeholder="Longitude"
|
placeholder="Longitude"
|
||||||
|
placeholderTextColor="#9ca3af"
|
||||||
value={newCoord ? String(newCoord.longitude.toFixed(6)) : ''}
|
value={newCoord ? String(newCoord.longitude.toFixed(6)) : ''}
|
||||||
editable={false}
|
editable={false}
|
||||||
/>
|
/>
|
||||||
@@ -310,6 +330,7 @@ const MapScreen = ({navigation}) => {
|
|||||||
<TextInput
|
<TextInput
|
||||||
style={styles.formInput}
|
style={styles.formInput}
|
||||||
placeholder="Claim Radius (metres)"
|
placeholder="Claim Radius (metres)"
|
||||||
|
placeholderTextColor="#9ca3af"
|
||||||
value={newRadius}
|
value={newRadius}
|
||||||
onChangeText={setNewRadius}
|
onChangeText={setNewRadius}
|
||||||
keyboardType="numeric"
|
keyboardType="numeric"
|
||||||
@@ -337,15 +358,17 @@ const MapScreen = ({navigation}) => {
|
|||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
return (
|
return (
|
||||||
<Screen style={styles.containerMap}>
|
<Screen style={styles.containerMap}>
|
||||||
<View style={styles.mapWrap}>
|
<View style={styles.mapContainer}>
|
||||||
<MapView
|
<MapView
|
||||||
style={{flex: 1}}
|
style={{flex: 1}}
|
||||||
initialRegion={mapRegion}
|
region={mapRegion}
|
||||||
|
scrollEnabled={true}
|
||||||
|
zoomEnabled={true}
|
||||||
showsUserLocation
|
showsUserLocation
|
||||||
>
|
>
|
||||||
{cacheRecords.map((cache) => (
|
{cacheRecords.map((cache) => (
|
||||||
<React.Fragment key={cache.id}>
|
<React.Fragment key={cache.id}>
|
||||||
<Marker coordinate={cache.coordinates} title={cache.clue}/>
|
<Marker coordinate={cache.coordinates} title={cache.name || cache.clue}/>
|
||||||
<Circle
|
<Circle
|
||||||
center={cache.coordinates}
|
center={cache.coordinates}
|
||||||
radius={cache.radius}
|
radius={cache.radius}
|
||||||
@@ -354,7 +377,23 @@ const MapScreen = ({navigation}) => {
|
|||||||
/>
|
/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
|
{userLocation && heading !== null && (
|
||||||
|
<Marker
|
||||||
|
coordinate={userLocation}
|
||||||
|
flat={true}
|
||||||
|
rotation={heading}
|
||||||
|
anchor={{x: 0.5, y: 1.0}}
|
||||||
|
tracksViewChanges={false}
|
||||||
|
>
|
||||||
|
<View style={styles.headingConeWrap}>
|
||||||
|
<View style={styles.headingCone}/>
|
||||||
|
</View>
|
||||||
|
</Marker>
|
||||||
|
)}
|
||||||
</MapView>
|
</MapView>
|
||||||
|
<Pressable style={styles.expandButton} onPress={handleExpandMap}>
|
||||||
|
<Text style={styles.expandIcon}>⛶</Text>
|
||||||
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.createCacheWrap}>
|
<View style={styles.createCacheWrap}>
|
||||||
<Button
|
<Button
|
||||||
@@ -364,8 +403,8 @@ const MapScreen = ({navigation}) => {
|
|||||||
styleLabel={styles.createCacheLabel}
|
styleLabel={styles.createCacheLabel}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.cacheSection}>
|
<View style={styles.cacheListWrap}>
|
||||||
<ScrollView>
|
<ScrollView contentContainerStyle={styles.cacheListContent}>
|
||||||
{cacheRecords.map((cache) => (
|
{cacheRecords.map((cache) => (
|
||||||
<CacheCardItem
|
<CacheCardItem
|
||||||
key={cache.id}
|
key={cache.id}
|
||||||
@@ -411,14 +450,21 @@ const MapScreen = ({navigation}) => {
|
|||||||
{teamsWarning && (
|
{teamsWarning && (
|
||||||
<Text style={styles.warning}>You are not in a team. Teams are required for this game!</Text>
|
<Text style={styles.warning}>You are not in a team. Teams are required for this game!</Text>
|
||||||
)}
|
)}
|
||||||
<View style={styles.mapWrap}>
|
<View style={styles.mapContainer}>
|
||||||
<PlayerMapView
|
<PlayerMapView
|
||||||
userLocation={userLocation}
|
userLocation={userLocation}
|
||||||
visibleCache={visibleCache}
|
visibleCache={visibleCache}
|
||||||
isClaiming={isClaiming}
|
heading={heading}
|
||||||
onClaimSuccess={handleClaim}
|
|
||||||
/>
|
/>
|
||||||
|
<Pressable style={styles.expandButton} onPress={handleExpandMap}>
|
||||||
|
<Text style={styles.expandIcon}>⛶</Text>
|
||||||
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
|
<ClaimTimerView
|
||||||
|
cache={visibleCache}
|
||||||
|
isClaiming={isClaiming}
|
||||||
|
onClaimSuccess={handleClaim}
|
||||||
|
/>
|
||||||
<View style={styles.cacheSection}>
|
<View style={styles.cacheSection}>
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
{cacheRecords.map((cache) => (
|
{cacheRecords.map((cache) => (
|
||||||
@@ -441,7 +487,7 @@ 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: 1},
|
mapContainer: {height: 200},
|
||||||
error: {color: '#dc2626', fontSize: 15},
|
error: {color: '#dc2626', fontSize: 15},
|
||||||
loadingText: {color: '#6b7280', fontSize: 14, marginTop: 10},
|
loadingText: {color: '#6b7280', fontSize: 14, marginTop: 10},
|
||||||
inputRow: {flexDirection: 'row', gap: 10, marginBottom: 15, width: '100%', paddingHorizontal: 20},
|
inputRow: {flexDirection: 'row', gap: 10, marginBottom: 15, width: '100%', paddingHorizontal: 20},
|
||||||
@@ -454,6 +500,8 @@ const styles = StyleSheet.create({
|
|||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
paddingVertical: 10,
|
paddingVertical: 10,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
color: '#1f2937',
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
},
|
},
|
||||||
joinButton: {backgroundColor: '#2563eb', borderColor: '#2563eb', flex: 0, paddingHorizontal: 20},
|
joinButton: {backgroundColor: '#2563eb', borderColor: '#2563eb', flex: 0, paddingHorizontal: 20},
|
||||||
joinLabel: {color: '#ffffff', fontWeight: '600'},
|
joinLabel: {color: '#ffffff', fontWeight: '600'},
|
||||||
@@ -466,9 +514,32 @@ const styles = StyleSheet.create({
|
|||||||
paddingVertical: 8,
|
paddingVertical: 8,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
},
|
},
|
||||||
createCacheWrap: {paddingHorizontal: 12, paddingVertical: 8},
|
expandButton: {
|
||||||
createCacheButton: {backgroundColor: '#2563eb', borderColor: '#2563eb'},
|
position: 'absolute',
|
||||||
|
top: 10,
|
||||||
|
left: 10,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.6)',
|
||||||
|
borderRadius: 6,
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 10,
|
||||||
|
},
|
||||||
|
expandIcon: {color: '#ffffff', fontSize: 18, fontWeight: '700'},
|
||||||
|
// Admin create button + scrollable cache list
|
||||||
|
createCacheWrap: {
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 10,
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: '#e5e7eb',
|
||||||
|
},
|
||||||
|
createCacheButton: {backgroundColor: '#2563eb', borderColor: '#2563eb', flex: 0},
|
||||||
createCacheLabel: {color: '#ffffff', fontWeight: '600'},
|
createCacheLabel: {color: '#ffffff', fontWeight: '600'},
|
||||||
|
cacheListWrap: {flex: 1, backgroundColor: '#f3f4f6'},
|
||||||
|
cacheListContent: {paddingHorizontal: 12, paddingTop: 10, paddingBottom: 12},
|
||||||
|
// Cache editor form
|
||||||
formSection: {flex: 1, paddingHorizontal: 12, paddingVertical: 10},
|
formSection: {flex: 1, paddingHorizontal: 12, paddingVertical: 10},
|
||||||
formInput: {
|
formInput: {
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
@@ -477,6 +548,7 @@ const styles = StyleSheet.create({
|
|||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
paddingVertical: 10,
|
paddingVertical: 10,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
color: '#1f2937',
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: '#ffffff',
|
||||||
marginBottom: 10,
|
marginBottom: 10,
|
||||||
},
|
},
|
||||||
@@ -487,13 +559,22 @@ const styles = StyleSheet.create({
|
|||||||
saveLabel: {color: '#ffffff', fontWeight: '600'},
|
saveLabel: {color: '#ffffff', fontWeight: '600'},
|
||||||
cancelButton: {backgroundColor: '#6b7280', borderColor: '#6b7280'},
|
cancelButton: {backgroundColor: '#6b7280', borderColor: '#6b7280'},
|
||||||
cancelLabel: {color: '#ffffff', fontWeight: '600'},
|
cancelLabel: {color: '#ffffff', fontWeight: '600'},
|
||||||
cacheSection: {
|
// Player cache section
|
||||||
maxHeight: 160,
|
cacheSection: {flex: 1, backgroundColor: '#f3f4f6', paddingHorizontal: 12, paddingTop: 10},
|
||||||
backgroundColor: '#f3f4f6',
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
paddingTop: 10,
|
|
||||||
},
|
|
||||||
emptyText: {color: '#9ca3af', textAlign: 'center', marginTop: 20, fontSize: 14},
|
emptyText: {color: '#9ca3af', textAlign: 'center', marginTop: 20, fontSize: 14},
|
||||||
|
mapWrap: {flex: 1},
|
||||||
|
// Heading direction cone (like Google Maps)
|
||||||
|
headingConeWrap: {width: 22, height: 24, alignItems: 'center'},
|
||||||
|
headingCone: {
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
borderTopWidth: 24,
|
||||||
|
borderLeftWidth: 11,
|
||||||
|
borderRightWidth: 11,
|
||||||
|
borderTopColor: 'rgba(37,99,235,0.70)',
|
||||||
|
borderLeftColor: 'transparent',
|
||||||
|
borderRightColor: 'transparent',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default MapScreen;
|
export default MapScreen;
|
||||||
@@ -10,7 +10,7 @@ const PlayersScreen = () => {
|
|||||||
// Initialisation ------------
|
// Initialisation ------------
|
||||||
|
|
||||||
const session = getSession();
|
const session = getSession();
|
||||||
const {getLobby, getTeams, getTeamMembers, getGroupMembers, getUser, getCaches} = useGameHook();
|
const {getLobby, getTeams, getTeamMembers, getGroupMembers, removeMember, getUser, getCaches} = useGameHook();
|
||||||
|
|
||||||
// State ----------------------
|
// State ----------------------
|
||||||
|
|
||||||
@@ -35,17 +35,15 @@ const PlayersScreen = () => {
|
|||||||
setGroupInfo(group);
|
setGroupInfo(group);
|
||||||
|
|
||||||
if (group && group.TeamsEnabled) {
|
if (group && group.TeamsEnabled) {
|
||||||
// Teams enabled — load all teams for this group
|
|
||||||
const teamRows = await getTeams(session.currentGid);
|
const teamRows = await getTeams(session.currentGid);
|
||||||
setTeams(teamRows || []);
|
setTeams(teamRows || []);
|
||||||
} else {
|
} else {
|
||||||
// Teams disabled — load all members for this group
|
const allMembers = await getGroupMembers(session.currentGid);
|
||||||
const memberships = await getGroupMembers(session.currentGid);
|
const nonAdminMembers = (allMembers || []).filter((m) => !m.IsAcceptedAdmin);
|
||||||
const nonAdminMembers = (memberships || []).filter((m) => !m.IsAcceptedAdmin);
|
|
||||||
const playerList = [];
|
const playerList = [];
|
||||||
for (const m of nonAdminMembers) {
|
for (const m of nonAdminMembers) {
|
||||||
const user = await getUser(m.Uid);
|
const user = await getUser(m.Uid);
|
||||||
if (user) playerList.push(user);
|
if (user) playerList.push({...user, membershipId: m.id});
|
||||||
}
|
}
|
||||||
setPlayers(playerList);
|
setPlayers(playerList);
|
||||||
}
|
}
|
||||||
@@ -58,7 +56,6 @@ const PlayersScreen = () => {
|
|||||||
setSelectedTeam(team);
|
setSelectedTeam(team);
|
||||||
setDetailLoading(true);
|
setDetailLoading(true);
|
||||||
|
|
||||||
// Load team members and their usernames
|
|
||||||
const ms = await getTeamMembers(team.Tid);
|
const ms = await getTeamMembers(team.Tid);
|
||||||
setTeamMembers(ms || []);
|
setTeamMembers(ms || []);
|
||||||
|
|
||||||
@@ -69,7 +66,6 @@ const PlayersScreen = () => {
|
|||||||
}
|
}
|
||||||
setMemberNames(names);
|
setMemberNames(names);
|
||||||
|
|
||||||
// Load all caches to count individual contributions
|
|
||||||
const caches = await getCaches(session.currentGid, null);
|
const caches = await getCaches(session.currentGid, null);
|
||||||
const counts = {};
|
const counts = {};
|
||||||
for (const m of (ms || [])) {
|
for (const m of (ms || [])) {
|
||||||
@@ -86,6 +82,21 @@ const PlayersScreen = () => {
|
|||||||
setMemberCacheCounts({});
|
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 -----------------------
|
// View -----------------------
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -130,6 +141,14 @@ const PlayersScreen = () => {
|
|||||||
{memberCacheCounts[m.Uid] || 0} cache(s) claimed
|
{memberCacheCounts[m.Uid] || 0} cache(s) claimed
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
<View style={styles.removeWrap}>
|
||||||
|
<Button
|
||||||
|
label="Remove"
|
||||||
|
onClick={() => handleRemoveTeamMember(m)}
|
||||||
|
styleButton={styles.removeButton}
|
||||||
|
styleLabel={styles.removeLabel}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
{teamMembers.length === 0 && (
|
{teamMembers.length === 0 && (
|
||||||
@@ -185,7 +204,17 @@ const PlayersScreen = () => {
|
|||||||
<ScrollView style={styles.listSection}>
|
<ScrollView style={styles.listSection}>
|
||||||
{players.map((player) => (
|
{players.map((player) => (
|
||||||
<Card key={player.Uid}>
|
<Card key={player.Uid}>
|
||||||
<Text style={styles.playerName}>{player.username}</Text>
|
<View style={styles.memberRow}>
|
||||||
|
<Text style={styles.playerName}>{player.username}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.removeWrap}>
|
||||||
|
<Button
|
||||||
|
label="Remove"
|
||||||
|
onClick={() => handleRemovePlayer(player.membershipId)}
|
||||||
|
styleButton={styles.removeButton}
|
||||||
|
styleLabel={styles.removeLabel}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
{players.length === 0 && (
|
{players.length === 0 && (
|
||||||
@@ -204,7 +233,7 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
color: '#1f2937',
|
color: '#1f2937',
|
||||||
marginBottom: 10,
|
marginBottom: 12,
|
||||||
paddingHorizontal: 15,
|
paddingHorizontal: 15,
|
||||||
paddingTop: 15,
|
paddingTop: 15,
|
||||||
},
|
},
|
||||||
@@ -233,6 +262,10 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
memberName: {fontSize: 16, fontWeight: '600', color: '#1f2937'},
|
memberName: {fontSize: 16, fontWeight: '600', color: '#1f2937'},
|
||||||
memberCaches: {fontSize: 13, fontWeight: '600', color: '#16a34a'},
|
memberCaches: {fontSize: 13, fontWeight: '600', color: '#16a34a'},
|
||||||
|
// Remove button
|
||||||
|
removeWrap: {marginTop: 8},
|
||||||
|
removeButton: {backgroundColor: '#dc2626', borderColor: '#dc2626', minHeight: 36},
|
||||||
|
removeLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13},
|
||||||
backWrap: {padding: 15},
|
backWrap: {padding: 15},
|
||||||
backButton: {backgroundColor: '#6b7280', borderColor: '#6b7280'},
|
backButton: {backgroundColor: '#6b7280', borderColor: '#6b7280'},
|
||||||
backLabel: {color: '#ffffff', fontWeight: '600'},
|
backLabel: {color: '#ffffff', fontWeight: '600'},
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ const RegisterScreen = ({navigation}) => {
|
|||||||
<TextInput
|
<TextInput
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
placeholder="Username"
|
placeholder="Username"
|
||||||
|
placeholderTextColor="#9ca3af"
|
||||||
value={username}
|
value={username}
|
||||||
onChangeText={setUsername}
|
onChangeText={setUsername}
|
||||||
editable={!isLoading}
|
editable={!isLoading}
|
||||||
@@ -49,6 +50,7 @@ const RegisterScreen = ({navigation}) => {
|
|||||||
<TextInput
|
<TextInput
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
placeholder="Email"
|
placeholder="Email"
|
||||||
|
placeholderTextColor="#9ca3af"
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
keyboardType="email-address"
|
keyboardType="email-address"
|
||||||
value={email}
|
value={email}
|
||||||
@@ -58,6 +60,7 @@ const RegisterScreen = ({navigation}) => {
|
|||||||
<TextInput
|
<TextInput
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
|
placeholderTextColor="#9ca3af"
|
||||||
secureTextEntry
|
secureTextEntry
|
||||||
value={password}
|
value={password}
|
||||||
onChangeText={setPassword}
|
onChangeText={setPassword}
|
||||||
@@ -66,6 +69,7 @@ const RegisterScreen = ({navigation}) => {
|
|||||||
<TextInput
|
<TextInput
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
placeholder="Confirm Password"
|
placeholder="Confirm Password"
|
||||||
|
placeholderTextColor="#9ca3af"
|
||||||
secureTextEntry
|
secureTextEntry
|
||||||
value={confirmPassword}
|
value={confirmPassword}
|
||||||
onChangeText={setConfirmPassword}
|
onChangeText={setConfirmPassword}
|
||||||
@@ -132,6 +136,8 @@ const styles = StyleSheet.create({
|
|||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
paddingVertical: 10,
|
paddingVertical: 10,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
color: '#1f2937',
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
},
|
},
|
||||||
typeRow: {
|
typeRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|||||||
@@ -160,6 +160,8 @@ const styles = StyleSheet.create({
|
|||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
paddingVertical: 10,
|
paddingVertical: 10,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
color: '#1f2937',
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
},
|
},
|
||||||
joinButton: {backgroundColor: '#2563eb', borderColor: '#2563eb', flex: 0, paddingHorizontal: 20},
|
joinButton: {backgroundColor: '#2563eb', borderColor: '#2563eb', flex: 0, paddingHorizontal: 20},
|
||||||
joinLabel: {color: '#ffffff', fontWeight: '600'},
|
joinLabel: {color: '#ffffff', fontWeight: '600'},
|
||||||
|
|||||||
@@ -69,6 +69,11 @@ const useGameHook = () => {
|
|||||||
return response.isSuccess ? response.result : [];
|
return response.isSuccess ? response.result : [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const removeMember = async (membershipId) => {
|
||||||
|
const response = await API.delete(`${subgroupMembershipsEndpoint}/${membershipId}`);
|
||||||
|
return response.isSuccess;
|
||||||
|
};
|
||||||
|
|
||||||
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;
|
||||||
@@ -156,6 +161,7 @@ const useGameHook = () => {
|
|||||||
joinPrivateGame,
|
joinPrivateGame,
|
||||||
joinAsAdmin,
|
joinAsAdmin,
|
||||||
getGroupMembers,
|
getGroupMembers,
|
||||||
|
removeMember,
|
||||||
getTeams,
|
getTeams,
|
||||||
getTeam,
|
getTeam,
|
||||||
createTeam,
|
createTeam,
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import * as geolib from 'geolib';
|
import * as geolib from 'geolib';
|
||||||
|
|
||||||
|
// Convert raw Magnetometer {x, y} reading to a 0-360° heading
|
||||||
|
const toHeading = ({x, y}) => {
|
||||||
|
const angle = Math.atan2(y, x) * (180 / Math.PI);
|
||||||
|
return angle < 0 ? angle + 360 : angle;
|
||||||
|
};
|
||||||
|
|
||||||
const isWithinRadius = (playerCoords, cacheCoords, radius) => {
|
const isWithinRadius = (playerCoords, cacheCoords, radius) => {
|
||||||
const distance = geolib.getDistance(playerCoords, cacheCoords);
|
const distance = geolib.getDistance(playerCoords, cacheCoords);
|
||||||
return distance <= radius;
|
return distance <= radius;
|
||||||
@@ -14,5 +20,5 @@ const isLookingAtCache = (playerHeading, playerCoords, cacheCoords, tolerance =
|
|||||||
return diff <= tolerance || diff >= 360 - tolerance;
|
return diff <= tolerance || diff >= 360 - tolerance;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { isWithinRadius, isLookingAtCache };
|
export { toHeading, isWithinRadius, isLookingAtCache };
|
||||||
|
|
||||||
Reference in new issue
Block a user