diff --git a/App.js b/App.js index 222b6a0..9efbd46 100644 --- a/App.js +++ b/App.js @@ -9,6 +9,7 @@ import TeamScreen from "./src/components/screens/TeamScreen"; import MapScreen from "./src/components/screens/MapScreen"; import LeaderboardScreen from "./src/components/screens/LeaderboardScreen"; import GameSettingsScreen from "./src/components/screens/GameSettingsScreen"; +import DepartmentSettingsScreen from "./src/components/screens/DepartmentSettingsScreen"; import PlayersScreen from "./src/components/screens/PlayersScreen"; import ExpandedMapScreen from "./src/components/screens/ExpandedMapScreen"; import {loadSession} from "./src/hooks/SessionStore"; @@ -43,6 +44,7 @@ const GameStack = () => ( const AdminStack = () => ( + diff --git a/assets/icons/Leaderboard-icon.png b/assets/icons/Leaderboard-icon.png new file mode 100644 index 0000000..e04806b Binary files /dev/null and b/assets/icons/Leaderboard-icon.png differ diff --git a/assets/icons/Map-icon.png b/assets/icons/Map-icon.png new file mode 100644 index 0000000..51d6f8a Binary files /dev/null and b/assets/icons/Map-icon.png differ diff --git a/assets/icons/Teams-icon.png b/assets/icons/Teams-icon.png new file mode 100644 index 0000000..8ca3dfc Binary files /dev/null and b/assets/icons/Teams-icon.png differ diff --git a/assets/icons/settings-icon.png b/assets/icons/settings-icon.png new file mode 100644 index 0000000..94ef7da Binary files /dev/null and b/assets/icons/settings-icon.png differ diff --git a/src/components/UI/Card.js b/src/components/UI/Card.js index 22d6d3f..e248505 100644 --- a/src/components/UI/Card.js +++ b/src/components/UI/Card.js @@ -13,12 +13,12 @@ const Card = ({children, style}) => { const styles = StyleSheet.create({ card: { - backgroundColor: '#ffffff', + backgroundColor: '#313244', borderRadius: 8, padding: 12, marginBottom: 8, borderWidth: 1, - borderColor: '#e5e7eb', + borderColor: '#45475a', }, }); diff --git a/src/components/gameplay/CacheCardItem.js b/src/components/gameplay/CacheCardItem.js index 519fb1e..f890da1 100644 --- a/src/components/gameplay/CacheCardItem.js +++ b/src/components/gameplay/CacheCardItem.js @@ -2,31 +2,28 @@ import {StyleSheet, Text, View} from 'react-native'; import Card from '../UI/Card'; import {Button} from '../UI/Button'; -const CacheCardItem = ({cache, isAdmin, isClaimed, isSelected, onEdit, onDelete, onSelect, disabled}) => { -// Initialisation ------------- +const CacheCardItem = ({cache, isAdmin, isClaimed, isSelected, onEdit, onDelete, onSelect, disabled, departmentName}) => { + // Display name fallback for cache without a name + const displayName = cache.name || cache.clue || 'Unnamed Cache'; - const displayName = cache.name || cache.clue || 'Unnamed Cache'; - -// State ---------------------- -// Handlers ------------------- -// View ----------------------- - - return ( - - - - {displayName} - {cache.name ? ( - {cache.clue} - ) : null} - - {/* Players see team-specific claim status; admins see no status */} - {!isAdmin && !disabled && ( - - {isClaimed ? 'Claimed' : 'Available'} - - )} - + return ( + + + + {displayName} + {isAdmin && departmentName ? ( + Department: {departmentName} + ) : null} + {cache.name ? ( + {cache.clue} + ) : null} + + {!isAdmin && !disabled && ( + + {isClaimed ? 'Claimed' : 'Available'} + + )} + {isAdmin ? ( <> @@ -86,54 +83,55 @@ const styles = StyleSheet.create({ alignItems: 'center', marginBottom: 8, }, - clue: {fontSize: 13, color: '#6b7280', marginTop: 2}, + clue: {fontSize: 13, color: '#9ca3af', marginTop: 2}, + departmentText: {fontSize: 13, color: '#bd93f9', marginTop: 2, fontWeight: '600'}, nameWrap: {flex: 1, marginRight: 8}, - name: {fontSize: 15, fontWeight: '600', color: '#1f2937'}, - claimedText: {color: '#9ca3af'}, + name: {fontSize: 15, fontWeight: '600', color: '#cdd6f4'}, + claimedText: {color: '#6c7086'}, status: {fontSize: 13, fontWeight: '600'}, actions: {flexDirection: 'row', gap: 8}, editBtn: { - backgroundColor: '#2563eb', - borderColor: '#2563eb', + backgroundColor: '#bd93f9', + borderColor: '#bd93f9', minHeight: 36, paddingHorizontal: 14, flex: 1, }, deleteBtn: { - backgroundColor: '#dc2626', - borderColor: '#dc2626', + backgroundColor: '#D92800', + borderColor: '#D92800', minHeight: 36, paddingHorizontal: 14, flex: 1, }, selectBtn: { - backgroundColor: '#16a34a', - borderColor: '#16a34a', + backgroundColor: '#a6e3a1', + borderColor: '#a6e3a1', minHeight: 36, paddingHorizontal: 14, flex: 1, }, selectedBtn: { - backgroundColor: '#6b7280', - borderColor: '#6b7280', + backgroundColor: '#6c7086', + borderColor: '#6c7086', minHeight: 36, paddingHorizontal: 14, flex: 1, }, claimedBtn: { - backgroundColor: '#d1d5db', - borderColor: '#d1d5db', + backgroundColor: '#45475a', + borderColor: '#45475a', minHeight: 36, paddingHorizontal: 14, flex: 1, }, - claimedBtnLabel: {color: '#6b7280', fontWeight: '600', fontSize: 13}, - selectedBtnLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13}, - selectedCard: {borderColor: '#2563eb', borderWidth: 2}, + claimedBtnLabel: {color: '#6c7086', fontWeight: '600', fontSize: 13}, + selectedBtnLabel: {color: '#cdd6f4', fontWeight: '600', fontSize: 13}, + selectedCard: {borderColor: '#bd93f9', borderWidth: 2}, claimedCard: {opacity: 0.5}, disabledCard: {opacity: 0.45}, - disabledText: {color: '#9ca3af'}, - btnLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13}, + disabledText: {color: '#6c7086'}, + btnLabel: {color: '#cdd6f4', fontWeight: '600', fontSize: 13}, }); export default CacheCardItem; diff --git a/src/components/gameplay/ClaimTimerView.js b/src/components/gameplay/ClaimTimerView.js index 6c97e29..45b8c0f 100644 --- a/src/components/gameplay/ClaimTimerView.js +++ b/src/components/gameplay/ClaimTimerView.js @@ -3,15 +3,11 @@ import {StyleSheet, Text, View} from 'react-native'; import {CACHE_CLAIM_TIMER} from '../../utils/geoMath'; const ClaimTimerView = ({ cache, onClaimSuccess, isClaiming, showClaimedPopup }) => { -// Initialisation ------------- -// State ---------------------- - const [timeLeft, setTimeLeft] = useState(CACHE_CLAIM_TIMER); // Use a ref for the callback so it does not appear in the dependency array const onClaimRef = useRef(onClaimSuccess); onClaimRef.current = onClaimSuccess; -// Handlers ------------------- useEffect(() => { let timer; @@ -25,7 +21,6 @@ const ClaimTimerView = ({ cache, onClaimSuccess, isClaiming, showClaimedPopup }) return () => clearTimeout(timer); }, [isClaiming, timeLeft, cache]); -// View ----------------------- // Show "Cache Claimed!" popup after a successful claim if (showClaimedPopup) { @@ -50,27 +45,27 @@ const ClaimTimerView = ({ cache, onClaimSuccess, isClaiming, showClaimedPopup }) const styles = StyleSheet.create({ overlay: { alignSelf: 'center', - backgroundColor: 'rgba(0,0,0,0.75)', + backgroundColor: 'rgba(0, 0, 0, 0.75)', paddingHorizontal: 16, paddingVertical: 12, borderRadius: 10, marginTop: 8, }, text: { - color: '#ffffff', + color: '#cdd6f4', fontSize: 16, fontWeight: '700', }, claimedOverlay: { alignSelf: 'center', - backgroundColor: '#16a34a', + backgroundColor: '#a6e3a1', paddingHorizontal: 20, paddingVertical: 14, borderRadius: 10, marginTop: 8, }, claimedText: { - color: '#ffffff', + color: '#1e1e2e', fontSize: 18, fontWeight: '700', }, diff --git a/src/components/layout/BottomNavbar.js b/src/components/layout/BottomNavbar.js index 3838578..85c96d7 100644 --- a/src/components/layout/BottomNavbar.js +++ b/src/components/layout/BottomNavbar.js @@ -3,17 +3,29 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'; import {clearGameSession, getSession} from '../../hooks/SessionStore'; import API, {API_BASE_URL} from '../API/API'; -const BottomNavbar = ({navigation, routeName}) => { +const BottomNavbar = ({navigation, routeName, routeParams}) => { // Initialisation ------------ const insets = useSafeAreaInsets(); const session = getSession(); + const selectedDepartment = routeName === 'DepartmentSettingsScreen' && routeParams?.SGid + ? {SGid: routeParams.SGid, SubGroupName: routeParams.SubGroupName || routeParams.selectedDepartmentName || 'Department'} + : null; + const adminTabs = [ {label: 'Settings', route: 'GameSettingsScreen', onPress: () => navigation.navigate('GameSettingsScreen')}, {label: 'Map', route: 'MapScreen', onPress: () => navigation.navigate('MapScreen')}, - {label: 'Players', route: 'PlayersScreen', onPress: () => navigation.navigate('PlayersScreen')}, - {label: 'Leaderboard', route: 'LeaderboardScreen', onPress: () => navigation.navigate('LeaderboardScreen')}, + { + label: 'Players', + route: 'PlayersScreen', + onPress: () => navigation.navigate('PlayersScreen', selectedDepartment ? {department: selectedDepartment} : undefined), + }, + { + label: 'Leaderboard', + route: 'LeaderboardScreen', + onPress: () => navigation.navigate('LeaderboardScreen', selectedDepartment ? {department: selectedDepartment} : undefined), + }, ]; // Only show the Leave tab when the player is actually in a game @@ -78,8 +90,8 @@ const styles = StyleSheet.create({ container: { minHeight: 64, borderTopWidth: 1, - borderTopColor: '#555555', - backgroundColor: '#4a4a4a', + borderTopColor: '#45475a', + backgroundColor: '#313244', flexDirection: 'row', alignItems: 'center', justifyContent: 'space-around', @@ -91,26 +103,25 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', paddingVertical: 12, - borderRadius: 8, marginHorizontal: 4, }, tabActive: { - backgroundColor: '#5c5c5c', + backgroundColor: '#45475a', }, tabLeave: { - backgroundColor: 'rgba(220,38,38,0.15)', + backgroundColor: 'rgba(243, 139, 168, 0.15)', }, tabText: { - color: '#d1d5db', + color: '#bac2de', fontSize: 12, fontWeight: '600', textAlign: 'center', }, tabTextActive: { - color: '#ffffff', + color: '#cdd6f4', }, tabTextLeave: { - color: '#fca5a5', + color: '#D92800', }, }); diff --git a/src/components/layout/Screen.js b/src/components/layout/Screen.js index 947fd9f..988a25a 100644 --- a/src/components/layout/Screen.js +++ b/src/components/layout/Screen.js @@ -17,6 +17,7 @@ const Screen = ({ children, style, showBack = false }) => { TeamScreen: "Current Team", MapScreen: "Map", GameSettingsScreen: "Game Settings", + DepartmentSettingsScreen: "Department Settings", PlayersScreen: "Players", ExpandedMapScreen: "Map View", }; @@ -34,7 +35,7 @@ const Screen = ({ children, style, showBack = false }) => { {!isAuthScreen && } {children} - {!isAuthScreen && } + {!isAuthScreen && } ); @@ -43,13 +44,13 @@ const Screen = ({ children, style, showBack = false }) => { const styles = StyleSheet.create({ screen: { flex: 1, - backgroundColor: "#fff", + backgroundColor: '#1e1e2e', }, content: { flex: 1, padding: 15, zIndex: 1, - overflow: "hidden", + overflow: 'hidden', }, }); diff --git a/src/components/layout/TopNavbar.js b/src/components/layout/TopNavbar.js index d39a291..b7bdd91 100644 --- a/src/components/layout/TopNavbar.js +++ b/src/components/layout/TopNavbar.js @@ -47,12 +47,12 @@ const styles = StyleSheet.create({ container: { minHeight: 56, borderBottomWidth: 1, - borderBottomColor: '#333333', + borderBottomColor: '#45475a', flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 12, - backgroundColor: '#000000', + backgroundColor: '#313244', zIndex: 10, }, leftSlot: { diff --git a/src/components/screens/DepartmentSettingsScreen.js b/src/components/screens/DepartmentSettingsScreen.js new file mode 100644 index 0000000..1d6824a --- /dev/null +++ b/src/components/screens/DepartmentSettingsScreen.js @@ -0,0 +1,224 @@ +import React, {useEffect, useState} from 'react'; +import {ActivityIndicator, Alert, 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'; + +const DepartmentSettingsScreen = ({route, navigation}) => { + const {SGid, Gid} = route.params; + const {getSubgroup, updateSubgroup, getCaches, resetGame} = useGameHook(); + + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [deptName, setDeptName] = useState(''); + const [joinCode, setJoinCode] = useState(''); + const [teamsEnabled, setTeamsEnabled] = useState(false); + const [cacheTriggerMeters, setCacheTriggerMeters] = useState('20'); + const [cacheCount, setCacheCount] = useState(0); + +// Handlers ------------------- + + useEffect(() => { + const load = async () => { + const sg = await getSubgroup(SGid); + if (sg) { + setDeptName(sg.SubGroupName || ''); + setJoinCode(sg.JoinCode || ''); + setTeamsEnabled(Boolean(sg.TeamsEnabled)); + setCacheTriggerMeters(String(sg.CacheTriggerMeters || 20)); + } + // Load cache count for this department + const caches = await getCaches(Gid, SGid); + setCacheCount((caches || []).length); + setLoading(false); + }; + load(); + }, []); + + const handleSave = async () => { + setSaving(true); + await updateSubgroup(SGid, { + SubGroupName: deptName.trim(), + TeamsEnabled: teamsEnabled, + CacheTriggerMeters: parseInt(cacheTriggerMeters) || 20, + }); + setSaving(false); + Alert.alert('Saved', 'Department settings updated.'); + }; + + const handleResetDepartment = () => { + Alert.alert( + 'Reset Department', + 'This will reset all cache claims for this department. Are you sure?', + [ + {text: 'Cancel', style: 'cancel'}, + { + text: 'Reset', + style: 'destructive', + onPress: async () => { + await resetGame(Gid, SGid); + Alert.alert('Done', 'Department progress has been reset.'); + }, + }, + ], + ); + }; + + const handleManageCaches = () => { + navigation.navigate('MapScreen', { + selectedDepartmentSGid: SGid, + selectedDepartmentName: deptName || null, + }); + }; + +// View ----------------------- + + if (loading) { + return ( + + + + ); + } + + return ( + + + Department Settings + + Department Name + + + + Department Join Code + {joinCode || '—'} + + + + Teams Enabled + + + + Cache Claim Distance (metres) + + + + Total Caches + {cacheCount} + + + + +