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
@@ -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 (
|
||||
<Card>
|
||||
<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>
|
||||
</View>
|
||||
<View style={styles.actions}>
|
||||
@@ -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: {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<MapView
|
||||
style={{flex: 1}}
|
||||
initialRegion={{
|
||||
...userLocation,
|
||||
latitudeDelta: 0.01,
|
||||
longitudeDelta: 0.01,
|
||||
}}
|
||||
showsUserLocation
|
||||
>
|
||||
{visibleCache ? (
|
||||
<Circle
|
||||
center={visibleCache.coordinates}
|
||||
radius={visibleCache.radius}
|
||||
fillColor="rgba(250, 204, 21, 0.20)"
|
||||
strokeColor="rgba(250, 204, 21, 0.90)"
|
||||
/>
|
||||
) : null}
|
||||
</MapView>
|
||||
|
||||
<ClaimTimerView
|
||||
cache={visibleCache}
|
||||
isClaiming={isClaiming}
|
||||
onClaimSuccess={onClaimSuccess}
|
||||
/>
|
||||
</>
|
||||
<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={visibleCache.radius}
|
||||
fillColor="rgba(250, 204, 21, 0.20)"
|
||||
strokeColor="rgba(250, 204, 21, 0.90)"
|
||||
/>
|
||||
) : null}
|
||||
{heading !== null && heading !== undefined && (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
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",
|
||||
GameSettingsScreen: "Game Settings",
|
||||
PlayersScreen: "Players",
|
||||
ExpandedMapScreen: "Map View",
|
||||
};
|
||||
|
||||
// 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 {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 (
|
||||
<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>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={groupName}
|
||||
onChangeText={setGroupName}
|
||||
placeholder="Enter game name"
|
||||
/>
|
||||
|
||||
<View style={styles.toggleRow}>
|
||||
<Text style={styles.label}>Teams Enabled</Text>
|
||||
<Switch
|
||||
value={teamsEnabled}
|
||||
onValueChange={setTeamsEnabled}
|
||||
trackColor={{false: '#d1d5db', true: '#93c5fd'}}
|
||||
thumbColor={teamsEnabled ? '#2563eb' : '#9ca3af'}
|
||||
<Text style={styles.label}>Game Name</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={groupName}
|
||||
onChangeText={setGroupName}
|
||||
placeholder="Enter game name"
|
||||
placeholderTextColor="#9ca3af"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{isBusiness && (
|
||||
<>
|
||||
<Text style={styles.label}>Max Member Subgroups</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={maxSubgroups}
|
||||
onChangeText={setMaxSubgroups}
|
||||
keyboardType="numeric"
|
||||
placeholder="1"
|
||||
<View style={styles.toggleRow}>
|
||||
<Text style={styles.toggleLabel}>Teams Enabled</Text>
|
||||
<Switch
|
||||
value={teamsEnabled}
|
||||
onValueChange={setTeamsEnabled}
|
||||
trackColor={{false: '#d1d5db', true: '#93c5fd'}}
|
||||
thumbColor={teamsEnabled ? '#2563eb' : '#9ca3af'}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.codeSection}>
|
||||
<Text style={styles.codeLabel}>Admin Join Code</Text>
|
||||
<Text style={styles.codeValue}>{adminJoinCode || '—'}</Text>
|
||||
</View>
|
||||
{isBusiness && (
|
||||
<>
|
||||
<Text style={styles.label}>Max Member Subgroups</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={maxSubgroups}
|
||||
onChangeText={setMaxSubgroups}
|
||||
keyboardType="numeric"
|
||||
placeholder="1"
|
||||
placeholderTextColor="#9ca3af"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<View style={styles.codeSection}>
|
||||
<Text style={styles.codeLabel}>Player Join Code</Text>
|
||||
<Text style={styles.codeValue}>{memberJoinCode || '—'}</Text>
|
||||
</View>
|
||||
<View style={styles.codeSection}>
|
||||
<Text style={styles.codeLabel}>Admin Join Code</Text>
|
||||
<Text style={styles.codeValue}>{adminJoinCode || '—'}</Text>
|
||||
</View>
|
||||
|
||||
<ButtonTray>
|
||||
<Button
|
||||
label={saving ? 'Saving...' : 'Save Settings'}
|
||||
onClick={handleSave}
|
||||
styleButton={styles.saveButton}
|
||||
styleLabel={styles.saveLabel}
|
||||
/>
|
||||
</ButtonTray>
|
||||
<View style={styles.codeSection}>
|
||||
<Text style={styles.codeLabel}>Player Join Code</Text>
|
||||
<Text style={styles.codeValue}>{memberJoinCode || '—'}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.saveWrap}>
|
||||
<ButtonTray>
|
||||
<Button
|
||||
label={saving ? 'Saving...' : 'Save Settings'}
|
||||
onClick={handleSave}
|
||||
styleButton={styles.saveButton}
|
||||
styleLabel={styles.saveLabel}
|
||||
/>
|
||||
</ButtonTray>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</Screen>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
center: {justifyContent: 'center', alignItems: 'center'},
|
||||
sectionTitle: {fontSize: 20, fontWeight: '700', color: '#1f2937', marginBottom: 15},
|
||||
label: {fontSize: 14, fontWeight: '600', color: '#374151', marginBottom: 4, marginTop: 10},
|
||||
formContainer: {paddingBottom: 20},
|
||||
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},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
@@ -143,29 +150,32 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
fontSize: 16,
|
||||
color: '#1f2937',
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
toggleRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginTop: 10,
|
||||
paddingVertical: 8,
|
||||
marginTop: 16,
|
||||
paddingVertical: 10,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#e5e7eb',
|
||||
},
|
||||
toggleLabel: {fontSize: 14, fontWeight: '600', color: '#374151'},
|
||||
codeSection: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginTop: 14,
|
||||
paddingVertical: 10,
|
||||
marginTop: 16,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 12,
|
||||
backgroundColor: '#f3f4f6',
|
||||
borderRadius: 8,
|
||||
},
|
||||
codeLabel: {fontSize: 14, fontWeight: '600', color: '#374151'},
|
||||
codeValue: {fontSize: 16, fontWeight: '700', color: '#2563eb', letterSpacing: 2},
|
||||
saveWrap: {marginTop: 24},
|
||||
saveButton: {backgroundColor: '#16a34a', borderColor: '#16a34a'},
|
||||
saveLabel: {color: '#ffffff', fontWeight: '600'},
|
||||
});
|
||||
|
||||
@@ -39,6 +39,7 @@ const LoginScreen = ({ navigation }) => {
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Email"
|
||||
placeholderTextColor="#9ca3af"
|
||||
autoCapitalize="none"
|
||||
keyboardType="email-address"
|
||||
value={email}
|
||||
@@ -48,6 +49,7 @@ const LoginScreen = ({ navigation }) => {
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Password"
|
||||
placeholderTextColor="#9ca3af"
|
||||
secureTextEntry
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
@@ -93,6 +95,8 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
fontSize: 16,
|
||||
color: '#1f2937',
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
errorText: {
|
||||
color: '#dc2626',
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
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 * 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';
|
||||
import ClaimTimerView from '../gameplay/ClaimTimerView';
|
||||
import PlayerMapView from '../gameplay/PlayerMapView';
|
||||
import useGameHook from '../../hooks/useGameHook';
|
||||
import usePlayerGame from '../../hooks/usePlayerGame';
|
||||
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 toHeading = ({x, y}) => {
|
||||
const angle = Math.atan2(y, x) * (180 / Math.PI);
|
||||
return angle < 0 ? angle + 360 : angle;
|
||||
};
|
||||
|
||||
const MapScreen = ({navigation}) => {
|
||||
// Initialisation ------------
|
||||
@@ -40,6 +38,7 @@ const MapScreen = ({navigation}) => {
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [editingCacheId, setEditingCacheId] = useState(null);
|
||||
const [newCoord, setNewCoord] = useState(null);
|
||||
const [newName, setNewName] = useState('');
|
||||
const [newClue, setNewClue] = useState('');
|
||||
const [newRadius, setNewRadius] = useState('20');
|
||||
|
||||
@@ -66,8 +65,7 @@ const MapScreen = ({navigation}) => {
|
||||
setCacheRecords([]);
|
||||
return;
|
||||
}
|
||||
const sgFilter = isAdmin ? null : session.currentSGid;
|
||||
const rows = await getCaches(session.currentGid, sgFilter);
|
||||
const rows = await getCaches(session.currentGid, null);
|
||||
setCacheRecords(rows || []);
|
||||
}, [inGame, isAdmin]);
|
||||
|
||||
@@ -94,10 +92,9 @@ const MapScreen = ({navigation}) => {
|
||||
(next) => setUserLocation({latitude: next.coords.latitude, longitude: next.coords.longitude}),
|
||||
);
|
||||
|
||||
if (isPlayer) {
|
||||
Magnetometer.setUpdateInterval(500);
|
||||
headingSub = Magnetometer.addListener((data) => setHeading(toHeading(data)));
|
||||
}
|
||||
// Track heading for all in-game users (admin + player)
|
||||
Magnetometer.setUpdateInterval(500);
|
||||
headingSub = Magnetometer.addListener((data) => setHeading(toHeading(data)));
|
||||
setPlayerLoading(false);
|
||||
};
|
||||
|
||||
@@ -152,6 +149,7 @@ const MapScreen = ({navigation}) => {
|
||||
const fallback = userLocation || {latitude: mapRegion.latitude, longitude: mapRegion.longitude};
|
||||
setEditingCacheId(null);
|
||||
setNewCoord(fallback);
|
||||
setNewName('');
|
||||
setNewClue('');
|
||||
setNewRadius('20');
|
||||
setIsCreating(true);
|
||||
@@ -161,6 +159,7 @@ const MapScreen = ({navigation}) => {
|
||||
const handleEditCache = (cache) => {
|
||||
setEditingCacheId(cache.id);
|
||||
setNewCoord(cache.coordinates);
|
||||
setNewName(cache.name || '');
|
||||
setNewClue(cache.clue || '');
|
||||
setNewRadius(String(cache.radius || 20));
|
||||
setIsCreating(true);
|
||||
@@ -178,6 +177,7 @@ const MapScreen = ({navigation}) => {
|
||||
if (!newCoord || !newClue.trim() || !session.currentGid) return;
|
||||
const payload = {
|
||||
gid: session.currentGid,
|
||||
name: newName.trim(),
|
||||
latitude: newCoord.latitude,
|
||||
longitude: newCoord.longitude,
|
||||
radius: parseInt(newRadius) || 20,
|
||||
@@ -196,7 +196,16 @@ const MapScreen = ({navigation}) => {
|
||||
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) => {
|
||||
// Future: scroll map to cache location
|
||||
};
|
||||
@@ -211,6 +220,7 @@ const MapScreen = ({navigation}) => {
|
||||
<TextInput
|
||||
style={styles.codeInput}
|
||||
placeholder="Enter Game Code"
|
||||
placeholderTextColor="#9ca3af"
|
||||
value={gameCode}
|
||||
onChangeText={setGameCode}
|
||||
autoCapitalize="characters"
|
||||
@@ -283,7 +293,15 @@ const MapScreen = ({navigation}) => {
|
||||
<ScrollView style={styles.formSection}>
|
||||
<TextInput
|
||||
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}
|
||||
onChangeText={setNewClue}
|
||||
/>
|
||||
@@ -293,6 +311,7 @@ const MapScreen = ({navigation}) => {
|
||||
<TextInput
|
||||
style={styles.formInput}
|
||||
placeholder="Latitude"
|
||||
placeholderTextColor="#9ca3af"
|
||||
value={newCoord ? String(newCoord.latitude.toFixed(6)) : ''}
|
||||
editable={false}
|
||||
/>
|
||||
@@ -302,6 +321,7 @@ const MapScreen = ({navigation}) => {
|
||||
<TextInput
|
||||
style={styles.formInput}
|
||||
placeholder="Longitude"
|
||||
placeholderTextColor="#9ca3af"
|
||||
value={newCoord ? String(newCoord.longitude.toFixed(6)) : ''}
|
||||
editable={false}
|
||||
/>
|
||||
@@ -310,6 +330,7 @@ const MapScreen = ({navigation}) => {
|
||||
<TextInput
|
||||
style={styles.formInput}
|
||||
placeholder="Claim Radius (metres)"
|
||||
placeholderTextColor="#9ca3af"
|
||||
value={newRadius}
|
||||
onChangeText={setNewRadius}
|
||||
keyboardType="numeric"
|
||||
@@ -337,15 +358,17 @@ const MapScreen = ({navigation}) => {
|
||||
if (isAdmin) {
|
||||
return (
|
||||
<Screen style={styles.containerMap}>
|
||||
<View style={styles.mapWrap}>
|
||||
<View style={styles.mapContainer}>
|
||||
<MapView
|
||||
style={{flex: 1}}
|
||||
initialRegion={mapRegion}
|
||||
region={mapRegion}
|
||||
scrollEnabled={true}
|
||||
zoomEnabled={true}
|
||||
showsUserLocation
|
||||
>
|
||||
{cacheRecords.map((cache) => (
|
||||
<React.Fragment key={cache.id}>
|
||||
<Marker coordinate={cache.coordinates} title={cache.clue}/>
|
||||
<Marker coordinate={cache.coordinates} title={cache.name || cache.clue}/>
|
||||
<Circle
|
||||
center={cache.coordinates}
|
||||
radius={cache.radius}
|
||||
@@ -354,7 +377,23 @@ const MapScreen = ({navigation}) => {
|
||||
/>
|
||||
</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>
|
||||
<Pressable style={styles.expandButton} onPress={handleExpandMap}>
|
||||
<Text style={styles.expandIcon}>⛶</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
<View style={styles.createCacheWrap}>
|
||||
<Button
|
||||
@@ -364,8 +403,8 @@ const MapScreen = ({navigation}) => {
|
||||
styleLabel={styles.createCacheLabel}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.cacheSection}>
|
||||
<ScrollView>
|
||||
<View style={styles.cacheListWrap}>
|
||||
<ScrollView contentContainerStyle={styles.cacheListContent}>
|
||||
{cacheRecords.map((cache) => (
|
||||
<CacheCardItem
|
||||
key={cache.id}
|
||||
@@ -411,14 +450,21 @@ const MapScreen = ({navigation}) => {
|
||||
{teamsWarning && (
|
||||
<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
|
||||
userLocation={userLocation}
|
||||
visibleCache={visibleCache}
|
||||
isClaiming={isClaiming}
|
||||
onClaimSuccess={handleClaim}
|
||||
heading={heading}
|
||||
/>
|
||||
<Pressable style={styles.expandButton} onPress={handleExpandMap}>
|
||||
<Text style={styles.expandIcon}>⛶</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
<ClaimTimerView
|
||||
cache={visibleCache}
|
||||
isClaiming={isClaiming}
|
||||
onClaimSuccess={handleClaim}
|
||||
/>
|
||||
<View style={styles.cacheSection}>
|
||||
<ScrollView>
|
||||
{cacheRecords.map((cache) => (
|
||||
@@ -441,7 +487,7 @@ const MapScreen = ({navigation}) => {
|
||||
const styles = StyleSheet.create({
|
||||
center: {justifyContent: 'center', alignItems: 'center'},
|
||||
containerMap: {padding: 0},
|
||||
mapWrap: {flex: 1},
|
||||
mapContainer: {height: 200},
|
||||
error: {color: '#dc2626', fontSize: 15},
|
||||
loadingText: {color: '#6b7280', fontSize: 14, marginTop: 10},
|
||||
inputRow: {flexDirection: 'row', gap: 10, marginBottom: 15, width: '100%', paddingHorizontal: 20},
|
||||
@@ -454,6 +500,8 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
fontSize: 16,
|
||||
color: '#1f2937',
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
joinButton: {backgroundColor: '#2563eb', borderColor: '#2563eb', flex: 0, paddingHorizontal: 20},
|
||||
joinLabel: {color: '#ffffff', fontWeight: '600'},
|
||||
@@ -466,9 +514,32 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: 8,
|
||||
fontSize: 14,
|
||||
},
|
||||
createCacheWrap: {paddingHorizontal: 12, paddingVertical: 8},
|
||||
createCacheButton: {backgroundColor: '#2563eb', borderColor: '#2563eb'},
|
||||
expandButton: {
|
||||
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'},
|
||||
cacheListWrap: {flex: 1, backgroundColor: '#f3f4f6'},
|
||||
cacheListContent: {paddingHorizontal: 12, paddingTop: 10, paddingBottom: 12},
|
||||
// Cache editor form
|
||||
formSection: {flex: 1, paddingHorizontal: 12, paddingVertical: 10},
|
||||
formInput: {
|
||||
borderWidth: 1,
|
||||
@@ -477,6 +548,7 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
fontSize: 16,
|
||||
color: '#1f2937',
|
||||
backgroundColor: '#ffffff',
|
||||
marginBottom: 10,
|
||||
},
|
||||
@@ -487,13 +559,22 @@ const styles = StyleSheet.create({
|
||||
saveLabel: {color: '#ffffff', fontWeight: '600'},
|
||||
cancelButton: {backgroundColor: '#6b7280', borderColor: '#6b7280'},
|
||||
cancelLabel: {color: '#ffffff', fontWeight: '600'},
|
||||
cacheSection: {
|
||||
maxHeight: 160,
|
||||
backgroundColor: '#f3f4f6',
|
||||
paddingHorizontal: 12,
|
||||
paddingTop: 10,
|
||||
},
|
||||
// Player cache section
|
||||
cacheSection: {flex: 1, backgroundColor: '#f3f4f6', paddingHorizontal: 12, paddingTop: 10},
|
||||
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;
|
||||
@@ -10,7 +10,7 @@ const PlayersScreen = () => {
|
||||
// Initialisation ------------
|
||||
|
||||
const session = getSession();
|
||||
const {getLobby, getTeams, getTeamMembers, getGroupMembers, getUser, getCaches} = useGameHook();
|
||||
const {getLobby, getTeams, getTeamMembers, getGroupMembers, removeMember, getUser, getCaches} = useGameHook();
|
||||
|
||||
// State ----------------------
|
||||
|
||||
@@ -35,17 +35,15 @@ const PlayersScreen = () => {
|
||||
setGroupInfo(group);
|
||||
|
||||
if (group && group.TeamsEnabled) {
|
||||
// Teams enabled — load all teams for this group
|
||||
const teamRows = await getTeams(session.currentGid);
|
||||
setTeams(teamRows || []);
|
||||
} else {
|
||||
// Teams disabled — load all members for this group
|
||||
const memberships = await getGroupMembers(session.currentGid);
|
||||
const nonAdminMembers = (memberships || []).filter((m) => !m.IsAcceptedAdmin);
|
||||
const 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);
|
||||
if (user) playerList.push({...user, membershipId: m.id});
|
||||
}
|
||||
setPlayers(playerList);
|
||||
}
|
||||
@@ -58,7 +56,6 @@ const PlayersScreen = () => {
|
||||
setSelectedTeam(team);
|
||||
setDetailLoading(true);
|
||||
|
||||
// Load team members and their usernames
|
||||
const ms = await getTeamMembers(team.Tid);
|
||||
setTeamMembers(ms || []);
|
||||
|
||||
@@ -69,7 +66,6 @@ const PlayersScreen = () => {
|
||||
}
|
||||
setMemberNames(names);
|
||||
|
||||
// Load all caches to count individual contributions
|
||||
const caches = await getCaches(session.currentGid, null);
|
||||
const counts = {};
|
||||
for (const m of (ms || [])) {
|
||||
@@ -86,6 +82,21 @@ const PlayersScreen = () => {
|
||||
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) {
|
||||
@@ -130,6 +141,14 @@ const PlayersScreen = () => {
|
||||
{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 && (
|
||||
@@ -185,7 +204,17 @@ const PlayersScreen = () => {
|
||||
<ScrollView style={styles.listSection}>
|
||||
{players.map((player) => (
|
||||
<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>
|
||||
))}
|
||||
{players.length === 0 && (
|
||||
@@ -204,7 +233,7 @@ const styles = StyleSheet.create({
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
color: '#1f2937',
|
||||
marginBottom: 10,
|
||||
marginBottom: 12,
|
||||
paddingHorizontal: 15,
|
||||
paddingTop: 15,
|
||||
},
|
||||
@@ -233,6 +262,10 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
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},
|
||||
removeLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13},
|
||||
backWrap: {padding: 15},
|
||||
backButton: {backgroundColor: '#6b7280', borderColor: '#6b7280'},
|
||||
backLabel: {color: '#ffffff', fontWeight: '600'},
|
||||
|
||||
@@ -42,6 +42,7 @@ const RegisterScreen = ({navigation}) => {
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Username"
|
||||
placeholderTextColor="#9ca3af"
|
||||
value={username}
|
||||
onChangeText={setUsername}
|
||||
editable={!isLoading}
|
||||
@@ -49,6 +50,7 @@ const RegisterScreen = ({navigation}) => {
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Email"
|
||||
placeholderTextColor="#9ca3af"
|
||||
autoCapitalize="none"
|
||||
keyboardType="email-address"
|
||||
value={email}
|
||||
@@ -58,6 +60,7 @@ const RegisterScreen = ({navigation}) => {
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Password"
|
||||
placeholderTextColor="#9ca3af"
|
||||
secureTextEntry
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
@@ -66,6 +69,7 @@ const RegisterScreen = ({navigation}) => {
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Confirm Password"
|
||||
placeholderTextColor="#9ca3af"
|
||||
secureTextEntry
|
||||
value={confirmPassword}
|
||||
onChangeText={setConfirmPassword}
|
||||
@@ -132,6 +136,8 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
fontSize: 16,
|
||||
color: '#1f2937',
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
typeRow: {
|
||||
flexDirection: 'row',
|
||||
|
||||
@@ -160,6 +160,8 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
fontSize: 16,
|
||||
color: '#1f2937',
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
joinButton: {backgroundColor: '#2563eb', borderColor: '#2563eb', flex: 0, paddingHorizontal: 20},
|
||||
joinLabel: {color: '#ffffff', fontWeight: '600'},
|
||||
|
||||
Reference in new issue
Block a user