mirror of
https://github.com/mudabbir-ahmad/MAD-Treasure-Hunt.git
synced 2026-10-08 03: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";
|
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 -------------
|
// Initialisation -------------
|
||||||
// State ----------------------
|
// State ----------------------
|
||||||
// Handlers -------------------
|
// Handlers -------------------
|
||||||
// View -----------------------
|
// View -----------------------
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable onPress={onClick} style={[styles.button, styleButton]}>
|
<Pressable onPress={onClick} disabled={disabled} style={[styles.button, styleButton, disabled && styles.buttonDisabled]}>
|
||||||
{icon ? icon : null}
|
{icon ? icon : null}
|
||||||
<Text style={[styles.label, styleLabel]}>{label}</Text>
|
<Text style={[styles.label, styleLabel, disabled && styles.labelDisabled]}>{label}</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -44,5 +44,12 @@ const styles = StyleSheet.create({
|
|||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
gap: 15,
|
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')},
|
{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 = [
|
const playerTabs = [
|
||||||
{label: 'Team', route: 'TeamScreen', onPress: () => navigation.navigate('TeamScreen')},
|
{label: 'Team', route: 'TeamScreen', onPress: () => navigation.navigate('TeamScreen')},
|
||||||
{label: 'Map', route: 'MapScreen', onPress: () => navigation.navigate('MapScreen')},
|
{label: 'Map', route: 'MapScreen', onPress: () => navigation.navigate('MapScreen')},
|
||||||
{label: 'Leaderboard', route: 'LeaderboardScreen', onPress: () => navigation.navigate('LeaderboardScreen')},
|
{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;
|
const tabs = session.isAcceptedAdmin ? adminTabs : playerTabs;
|
||||||
|
|||||||
@@ -49,9 +49,13 @@ const MapScreen = ({navigation}) => {
|
|||||||
|
|
||||||
const isPlayer = inGame && !isAdmin;
|
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)
|
// Filter out caches already claimed by the current user's team (or by the user if no team)
|
||||||
const activeCachesForPlayer = useMemo(() => {
|
const activeCachesForPlayer = useMemo(() => {
|
||||||
if (!isPlayer) return [];
|
if (!isPlayer) return EMPTY_CACHES;
|
||||||
return cacheRecords.filter((cache) => {
|
return cacheRecords.filter((cache) => {
|
||||||
const claims = cache.Claims || [];
|
const claims = cache.Claims || [];
|
||||||
if (session.currentTid) {
|
if (session.currentTid) {
|
||||||
@@ -59,12 +63,12 @@ const MapScreen = ({navigation}) => {
|
|||||||
}
|
}
|
||||||
return !claims.some((c) => c.Uid === session.currentUid);
|
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(
|
const {visibleCaches, isClaiming, setIsClaiming} = usePlayerGame(
|
||||||
isPlayer ? userLocation : null,
|
isPlayer ? userLocation : null,
|
||||||
isPlayer ? heading : null,
|
isPlayer ? heading : null,
|
||||||
isPlayer ? activeCachesForPlayer : [],
|
activeCachesForPlayer,
|
||||||
claimDistance,
|
claimDistance,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const PlayersScreen = () => {
|
|||||||
// Initialisation ------------
|
// Initialisation ------------
|
||||||
|
|
||||||
const session = getSession();
|
const session = getSession();
|
||||||
const {getTeams, getTeamMembers, getGroupMembers, removeMember, resetPlayerProgress, getUser, getAdminWaitlist} = useGameHook();
|
const {getTeams, getTeamMembers, getGroupMembers, removeMember, resetPlayerProgress, getUser, getAdminWaitlist, approveAdmin, rejectAdmin} = useGameHook();
|
||||||
|
|
||||||
// State ----------------------
|
// State ----------------------
|
||||||
|
|
||||||
@@ -28,6 +28,13 @@ const PlayersScreen = () => {
|
|||||||
// Get admin waitlist entries
|
// Get admin waitlist entries
|
||||||
const waitlist = await getAdminWaitlist(session.currentGid);
|
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 } }
|
// Build team lookup: { Uid -> { teamCode, teamName, isLeader } }
|
||||||
const allTeams = await getTeams(session.currentGid);
|
const allTeams = await getTeams(session.currentGid);
|
||||||
const teamInfoByUid = {};
|
const teamInfoByUid = {};
|
||||||
@@ -44,23 +51,33 @@ const PlayersScreen = () => {
|
|||||||
|
|
||||||
// Build player list — include admins with tags
|
// Build player list — include admins with tags
|
||||||
const playerList = [];
|
const playerList = [];
|
||||||
for (const m of (allMembers || [])) {
|
for (const m of uniqueMembers) {
|
||||||
// Skip the current admin viewing the list
|
// Skip the current admin viewing the list
|
||||||
if (m.Uid === session.currentUid) continue;
|
if (m.Uid === session.currentUid) continue;
|
||||||
const user = await getUser(m.Uid);
|
const user = await getUser(m.Uid);
|
||||||
if (user) {
|
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;
|
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({
|
playerList.push({
|
||||||
...user,
|
...user,
|
||||||
membershipId: m.id,
|
membershipId: m.id,
|
||||||
team: teamInfoByUid[m.Uid] || null,
|
team: teamInfoByUid[m.Uid] || null,
|
||||||
adminTag,
|
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 || [])) {
|
for (const w of (waitlist || [])) {
|
||||||
if (playerList.some((p) => p.Uid === w.Uid)) continue;
|
if (playerList.some((p) => p.Uid === w.Uid)) continue;
|
||||||
if (w.Uid === session.currentUid) 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 -----------------------
|
// View -----------------------
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -128,7 +157,8 @@ const PlayersScreen = () => {
|
|||||||
<Text style={styles.sectionTitle}>Players</Text>
|
<Text style={styles.sectionTitle}>Players</Text>
|
||||||
<ScrollView style={styles.listSection}>
|
<ScrollView style={styles.listSection}>
|
||||||
{players.map((player) => (
|
{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}>
|
<View style={styles.playerRow}>
|
||||||
<Text style={styles.playerName} numberOfLines={1}>
|
<Text style={styles.playerName} numberOfLines={1}>
|
||||||
{player.username}
|
{player.username}
|
||||||
@@ -148,21 +178,41 @@ const PlayersScreen = () => {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.actionRow}>
|
<View style={styles.actionRow}>
|
||||||
{!player.adminTag && (
|
{/* Waitlisted admins get Approve / Deny instead of the normal player actions */}
|
||||||
<Button
|
{player.waitlistId ? (
|
||||||
label="Reset Progress"
|
<>
|
||||||
onClick={() => handleResetPlayerProgress(player)}
|
<Button
|
||||||
styleButton={styles.resetButton}
|
label="Approve"
|
||||||
styleLabel={styles.actionLabel}
|
onClick={() => handleApproveAdmin(player)}
|
||||||
/>
|
styleButton={styles.approveButton}
|
||||||
)}
|
styleLabel={styles.actionLabel}
|
||||||
{player.membershipId && (
|
/>
|
||||||
<Button
|
<Button
|
||||||
label="Kick"
|
label="Deny"
|
||||||
onClick={() => handleRemovePlayer(player.membershipId)}
|
onClick={() => handleDenyAdmin(player)}
|
||||||
styleButton={styles.removeButton}
|
styleButton={styles.denyButton}
|
||||||
styleLabel={styles.actionLabel}
|
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>
|
</View>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -205,6 +255,11 @@ const styles = StyleSheet.create({
|
|||||||
resetButton: {backgroundColor: '#f59e0b', borderColor: '#f59e0b', minHeight: 36, flex: 1, paddingHorizontal: 10},
|
resetButton: {backgroundColor: '#f59e0b', borderColor: '#f59e0b', minHeight: 36, flex: 1, paddingHorizontal: 10},
|
||||||
removeButton: {backgroundColor: '#dc2626', borderColor: '#dc2626', minHeight: 36, flex: 1, paddingHorizontal: 10},
|
removeButton: {backgroundColor: '#dc2626', borderColor: '#dc2626', minHeight: 36, flex: 1, paddingHorizontal: 10},
|
||||||
actionLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13},
|
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;
|
export default PlayersScreen;
|
||||||
@@ -137,22 +137,30 @@ const TeamScreen = () => {
|
|||||||
|
|
||||||
// Not in a team
|
// Not in a team
|
||||||
if (!activeTid || !team) {
|
if (!activeTid || !team) {
|
||||||
|
// Disable team actions until the user has joined a game
|
||||||
|
const notInGame = !session.currentGid;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Screen style={styles.center}>
|
<Screen style={styles.center}>
|
||||||
|
{notInGame && (
|
||||||
|
<Text style={styles.notInGameText}>Join a game first to manage teams.</Text>
|
||||||
|
)}
|
||||||
<View style={styles.inputRow}>
|
<View style={styles.inputRow}>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={styles.codeInput}
|
style={[styles.codeInput, notInGame && styles.inputDisabled]}
|
||||||
placeholder="Enter Team Code"
|
placeholder="Enter Team Code"
|
||||||
placeholderTextColor="#9ca3af"
|
placeholderTextColor="#9ca3af"
|
||||||
value={teamCode}
|
value={teamCode}
|
||||||
onChangeText={(text) => setTeamCode(text.toUpperCase())}
|
onChangeText={(text) => setTeamCode(text.toUpperCase())}
|
||||||
autoCapitalize="characters"
|
autoCapitalize="characters"
|
||||||
|
editable={!notInGame}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
label="Join"
|
label="Join"
|
||||||
onClick={handleJoinTeam}
|
onClick={handleJoinTeam}
|
||||||
styleButton={styles.joinButton}
|
styleButton={styles.joinButton}
|
||||||
styleLabel={styles.joinLabel}
|
styleLabel={styles.joinLabel}
|
||||||
|
disabled={notInGame}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
{!isAdmin && (
|
{!isAdmin && (
|
||||||
@@ -163,6 +171,7 @@ const TeamScreen = () => {
|
|||||||
onClick={handleCreateTeam}
|
onClick={handleCreateTeam}
|
||||||
styleButton={styles.createButton}
|
styleButton={styles.createButton}
|
||||||
styleLabel={styles.createLabel}
|
styleLabel={styles.createLabel}
|
||||||
|
disabled={notInGame}
|
||||||
/>
|
/>
|
||||||
</ButtonTray>
|
</ButtonTray>
|
||||||
</View>
|
</View>
|
||||||
@@ -261,6 +270,9 @@ const TeamScreen = () => {
|
|||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
center: {justifyContent: 'center', alignItems: 'center'},
|
center: {justifyContent: 'center', alignItems: 'center'},
|
||||||
container: {padding: 0},
|
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},
|
inputRow: {flexDirection: 'row', gap: 10, marginBottom: 15, width: '100%', paddingHorizontal: 20},
|
||||||
fullRow: {width: '100%', paddingHorizontal: 20},
|
fullRow: {width: '100%', paddingHorizontal: 20},
|
||||||
codeInput: {
|
codeInput: {
|
||||||
|
|||||||
Reference in new issue
Block a user