diff --git a/src/components/UI/Button.js b/src/components/UI/Button.js index 3c39ec2..efa099d 100644 --- a/src/components/UI/Button.js +++ b/src/components/UI/Button.js @@ -1,15 +1,15 @@ import {Pressable, StyleSheet, Text, View} from "react-native"; -export const Button = ({ label, icon, onClick, styleLabel, styleButton }) => { +export const Button = ({ label, icon, onClick, styleLabel, styleButton, disabled }) => { // Initialisation ------------- // State ---------------------- // Handlers ------------------- // View ----------------------- return ( - + {icon ? icon : null} - {label} + {label} ); }; @@ -44,5 +44,12 @@ const styles = StyleSheet.create({ flexDirection: "row", gap: 15, }, + // Greyed-out state for buttons that should not be interactive + buttonDisabled: { + opacity: 0.4, + }, + labelDisabled: { + color: '#9ca3af', + }, }); diff --git a/src/components/layout/BottomNavbar.js b/src/components/layout/BottomNavbar.js index 7fd1d48..48f33bf 100644 --- a/src/components/layout/BottomNavbar.js +++ b/src/components/layout/BottomNavbar.js @@ -16,11 +16,14 @@ const BottomNavbar = ({navigation, routeName}) => { {label: 'Leaderboard', route: 'LeaderboardScreen', onPress: () => navigation.navigate('LeaderboardScreen')}, ]; + // Only show the Leave tab when the player is actually in a game + const inGame = Boolean(session.currentGid); + const playerTabs = [ {label: 'Team', route: 'TeamScreen', onPress: () => navigation.navigate('TeamScreen')}, {label: 'Map', route: 'MapScreen', onPress: () => navigation.navigate('MapScreen')}, {label: 'Leaderboard', route: 'LeaderboardScreen', onPress: () => navigation.navigate('LeaderboardScreen')}, - {label: 'Leave', route: '__leave__', onPress: () => handleLeaveGame()}, + ...(inGame ? [{label: 'Leave', route: '__leave__', onPress: () => handleLeaveGame()}] : []), ]; const tabs = session.isAcceptedAdmin ? adminTabs : playerTabs; diff --git a/src/components/screens/MapScreen.js b/src/components/screens/MapScreen.js index 48fb12e..890affb 100644 --- a/src/components/screens/MapScreen.js +++ b/src/components/screens/MapScreen.js @@ -49,9 +49,13 @@ const MapScreen = ({navigation}) => { const isPlayer = inGame && !isAdmin; + // Stable empty array — avoids creating a new reference on every render + // which would trigger an infinite re-render loop in usePlayerGame + const EMPTY_CACHES = useMemo(() => [], []); + // Filter out caches already claimed by the current user's team (or by the user if no team) const activeCachesForPlayer = useMemo(() => { - if (!isPlayer) return []; + if (!isPlayer) return EMPTY_CACHES; return cacheRecords.filter((cache) => { const claims = cache.Claims || []; if (session.currentTid) { @@ -59,12 +63,12 @@ const MapScreen = ({navigation}) => { } return !claims.some((c) => c.Uid === session.currentUid); }); - }, [cacheRecords, isPlayer, session.currentTid, session.currentUid]); + }, [cacheRecords, isPlayer, session.currentTid, session.currentUid, EMPTY_CACHES]); const {visibleCaches, isClaiming, setIsClaiming} = usePlayerGame( isPlayer ? userLocation : null, isPlayer ? heading : null, - isPlayer ? activeCachesForPlayer : [], + activeCachesForPlayer, claimDistance, ); diff --git a/src/components/screens/PlayersScreen.js b/src/components/screens/PlayersScreen.js index f37c222..86ab0c8 100644 --- a/src/components/screens/PlayersScreen.js +++ b/src/components/screens/PlayersScreen.js @@ -10,7 +10,7 @@ const PlayersScreen = () => { // Initialisation ------------ const session = getSession(); - const {getTeams, getTeamMembers, getGroupMembers, removeMember, resetPlayerProgress, getUser, getAdminWaitlist} = useGameHook(); + const {getTeams, getTeamMembers, getGroupMembers, removeMember, resetPlayerProgress, getUser, getAdminWaitlist, approveAdmin, rejectAdmin} = useGameHook(); // State ---------------------- @@ -28,6 +28,13 @@ const PlayersScreen = () => { // Get admin waitlist entries const waitlist = await getAdminWaitlist(session.currentGid); + // Deduplicate allMembers by Uid — a user may have two rows if they were a + // regular member before being approved as an admin. Prefer the admin row. + const seenUids = new Set(); + const uniqueMembers = (allMembers || []) + .sort((a, b) => (b.IsAcceptedAdmin ? 1 : 0) - (a.IsAcceptedAdmin ? 1 : 0)) + .filter((m) => { if (seenUids.has(m.Uid)) return false; seenUids.add(m.Uid); return true; }); + // Build team lookup: { Uid -> { teamCode, teamName, isLeader } } const allTeams = await getTeams(session.currentGid); const teamInfoByUid = {}; @@ -44,23 +51,33 @@ const PlayersScreen = () => { // Build player list — include admins with tags const playerList = []; - for (const m of (allMembers || [])) { + for (const m of uniqueMembers) { // Skip the current admin viewing the list if (m.Uid === session.currentUid) continue; const user = await getUser(m.Uid); if (user) { + // A group member may also be on the admin waitlist (applied after joining as a player). + // Check the waitlist first so they are treated as pending, not a regular player. + const waitlistEntry = (waitlist || []).find((w) => w.Uid === m.Uid); let adminTag = null; - if (m.IsAcceptedAdmin) adminTag = '[ADMIN]'; + let waitlistId = null; + if (waitlistEntry) { + adminTag = '[Admin Awaiting Response]'; + waitlistId = waitlistEntry.id; + } else if (m.IsAcceptedAdmin) { + adminTag = '[ADMIN]'; + } playerList.push({ ...user, membershipId: m.id, team: teamInfoByUid[m.Uid] || null, adminTag, + waitlistId, }); } } - // Include waitlisted users who are not yet group members + // Include waitlisted users who are not yet group members at all for (const w of (waitlist || [])) { if (playerList.some((p) => p.Uid === w.Uid)) continue; if (w.Uid === session.currentUid) continue; @@ -105,6 +122,18 @@ const PlayersScreen = () => { ); }; + // Approve a waitlisted admin and refresh the list + const handleApproveAdmin = async (player) => { + await approveAdmin(player.waitlistId); + await loadData(); + }; + + // Deny (remove from waitlist) a pending admin and refresh the list + const handleDenyAdmin = async (player) => { + await rejectAdmin(player.waitlistId); + await loadData(); + }; + // View ----------------------- if (loading) { @@ -128,7 +157,8 @@ const PlayersScreen = () => { Players {players.map((player) => ( - + // Waitlisted admins get a light red tint so they stand out from regular players + {player.username} @@ -148,21 +178,41 @@ const PlayersScreen = () => { - {!player.adminTag && ( -