mirror of
https://github.com/mudabbir-ahmad/MAD-Treasure-Hunt.git
synced 2026-10-07 19:00:20 +00:00
Enhance player and team management: add admin approval/denial for waitlisted users, conditionally render UI elements based on game status, and improve button states for better user experience
This commit is contained in:
1 parent
70ed43f12b
commit
d60f428383
5 files changed
+109
-28
No files matched your search
@@ -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 (
|
||||
<Pressable onPress={onClick} style={[styles.button, styleButton]}>
|
||||
<Pressable onPress={onClick} disabled={disabled} style={[styles.button, styleButton, disabled && styles.buttonDisabled]}>
|
||||
{icon ? icon : null}
|
||||
<Text style={[styles.label, styleLabel]}>{label}</Text>
|
||||
<Text style={[styles.label, styleLabel, disabled && styles.labelDisabled]}>{label}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
@@ -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 = () => {
|
||||
<Text style={styles.sectionTitle}>Players</Text>
|
||||
<ScrollView style={styles.listSection}>
|
||||
{players.map((player) => (
|
||||
<Card key={player.Uid}>
|
||||
// Waitlisted admins get a light red tint so they stand out from regular players
|
||||
<Card key={player.Uid} style={player.waitlistId ? styles.waitlistCard : undefined}>
|
||||
<View style={styles.playerRow}>
|
||||
<Text style={styles.playerName} numberOfLines={1}>
|
||||
{player.username}
|
||||
@@ -148,21 +178,41 @@ const PlayersScreen = () => {
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.actionRow}>
|
||||
{!player.adminTag && (
|
||||
<Button
|
||||
label="Reset Progress"
|
||||
onClick={() => handleResetPlayerProgress(player)}
|
||||
styleButton={styles.resetButton}
|
||||
styleLabel={styles.actionLabel}
|
||||
/>
|
||||
)}
|
||||
{player.membershipId && (
|
||||
<Button
|
||||
label="Kick"
|
||||
onClick={() => handleRemovePlayer(player.membershipId)}
|
||||
styleButton={styles.removeButton}
|
||||
styleLabel={styles.actionLabel}
|
||||
/>
|
||||
{/* Waitlisted admins get Approve / Deny instead of the normal player actions */}
|
||||
{player.waitlistId ? (
|
||||
<>
|
||||
<Button
|
||||
label="Approve"
|
||||
onClick={() => handleApproveAdmin(player)}
|
||||
styleButton={styles.approveButton}
|
||||
styleLabel={styles.actionLabel}
|
||||
/>
|
||||
<Button
|
||||
label="Deny"
|
||||
onClick={() => handleDenyAdmin(player)}
|
||||
styleButton={styles.denyButton}
|
||||
styleLabel={styles.actionLabel}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{!player.adminTag && (
|
||||
<Button
|
||||
label="Reset Progress"
|
||||
onClick={() => handleResetPlayerProgress(player)}
|
||||
styleButton={styles.resetButton}
|
||||
styleLabel={styles.actionLabel}
|
||||
/>
|
||||
)}
|
||||
{player.membershipId && (
|
||||
<Button
|
||||
label="Kick"
|
||||
onClick={() => handleRemovePlayer(player.membershipId)}
|
||||
styleButton={styles.removeButton}
|
||||
styleLabel={styles.actionLabel}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</Card>
|
||||
@@ -205,6 +255,11 @@ const styles = StyleSheet.create({
|
||||
resetButton: {backgroundColor: '#f59e0b', borderColor: '#f59e0b', minHeight: 36, flex: 1, paddingHorizontal: 10},
|
||||
removeButton: {backgroundColor: '#dc2626', borderColor: '#dc2626', minHeight: 36, flex: 1, paddingHorizontal: 10},
|
||||
actionLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13},
|
||||
// Waitlisted admin card tint — light red so they read as pending, not regular players
|
||||
waitlistCard: {backgroundColor: '#FFD1DC', borderColor: '#fca5a5'},
|
||||
// Approve / deny buttons for waitlisted admins
|
||||
approveButton: {backgroundColor: '#16a34a', borderColor: '#16a34a', minHeight: 36, flex: 1, paddingHorizontal: 10},
|
||||
denyButton: {backgroundColor: '#dc2626', borderColor: '#dc2626', minHeight: 36, flex: 1, paddingHorizontal: 10},
|
||||
});
|
||||
|
||||
export default PlayersScreen;
|
||||
@@ -137,22 +137,30 @@ const TeamScreen = () => {
|
||||
|
||||
// Not in a team
|
||||
if (!activeTid || !team) {
|
||||
// Disable team actions until the user has joined a game
|
||||
const notInGame = !session.currentGid;
|
||||
|
||||
return (
|
||||
<Screen style={styles.center}>
|
||||
{notInGame && (
|
||||
<Text style={styles.notInGameText}>Join a game first to manage teams.</Text>
|
||||
)}
|
||||
<View style={styles.inputRow}>
|
||||
<TextInput
|
||||
style={styles.codeInput}
|
||||
style={[styles.codeInput, notInGame && styles.inputDisabled]}
|
||||
placeholder="Enter Team Code"
|
||||
placeholderTextColor="#9ca3af"
|
||||
value={teamCode}
|
||||
onChangeText={(text) => setTeamCode(text.toUpperCase())}
|
||||
autoCapitalize="characters"
|
||||
editable={!notInGame}
|
||||
/>
|
||||
<Button
|
||||
label="Join"
|
||||
onClick={handleJoinTeam}
|
||||
styleButton={styles.joinButton}
|
||||
styleLabel={styles.joinLabel}
|
||||
disabled={notInGame}
|
||||
/>
|
||||
</View>
|
||||
{!isAdmin && (
|
||||
@@ -163,6 +171,7 @@ const TeamScreen = () => {
|
||||
onClick={handleCreateTeam}
|
||||
styleButton={styles.createButton}
|
||||
styleLabel={styles.createLabel}
|
||||
disabled={notInGame}
|
||||
/>
|
||||
</ButtonTray>
|
||||
</View>
|
||||
@@ -261,6 +270,9 @@ const TeamScreen = () => {
|
||||
const styles = StyleSheet.create({
|
||||
center: {justifyContent: 'center', alignItems: 'center'},
|
||||
container: {padding: 0},
|
||||
// Shown when the user has not yet joined a game
|
||||
notInGameText: {color: '#6b7280', fontSize: 15, fontWeight: '600', marginBottom: 16, textAlign: 'center', paddingHorizontal: 20},
|
||||
inputDisabled: {backgroundColor: '#f3f4f6', opacity: 0.5},
|
||||
inputRow: {flexDirection: 'row', gap: 10, marginBottom: 15, width: '100%', paddingHorizontal: 20},
|
||||
fullRow: {width: '100%', paddingHorizontal: 20},
|
||||
codeInput: {
|
||||
|
||||
Reference in new issue
Block a user