diff --git a/src/components/UI/CodeJoinRow.js b/src/components/UI/CodeJoinRow.js
index 1f94daf..d2772c5 100644
--- a/src/components/UI/CodeJoinRow.js
+++ b/src/components/UI/CodeJoinRow.js
@@ -1,11 +1,11 @@
-import {StyleSheet, TextInput, View} from 'react-native';
-import {Button} from './Button';
+import { StyleSheet, TextInput, View } from "react-native";
+import { Button } from "./Button";
const CodeJoinRow = ({
value,
onChange,
placeholder,
- buttonLabel = 'Join',
+ buttonLabel = "Join",
onSubmit,
disabled = false,
style,
@@ -39,37 +39,36 @@ const CodeJoinRow = ({
const styles = StyleSheet.create({
inputRow: {
- flexDirection: 'row',
+ flexDirection: "row",
gap: 10,
marginBottom: 15,
- width: '100%',
+ width: "100%",
paddingHorizontal: 20,
},
codeInput: {
flex: 1,
borderWidth: 1,
- borderColor: '#45475a',
+ borderColor: "#45475a",
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 10,
fontSize: 16,
- color: '#cdd6f4',
- backgroundColor: '#313244',
+ color: "#cdd6f4",
+ backgroundColor: "#313244",
},
inputDisabled: {
opacity: 0.5,
},
joinButton: {
- backgroundColor: '#bd93f9',
- borderColor: '#bd93f9',
+ backgroundColor: "#bd93f9",
+ borderColor: "#bd93f9",
flex: 0,
paddingHorizontal: 20,
},
joinLabel: {
- color: '#1e1e2e',
- fontWeight: '600',
+ color: "#1e1e2e",
+ fontWeight: "600",
},
});
export default CodeJoinRow;
-
diff --git a/src/components/gameplay/ClaimTimerView.js b/src/components/gameplay/ClaimTimerView.js
index 1c52826..b9d2c59 100644
--- a/src/components/gameplay/ClaimTimerView.js
+++ b/src/components/gameplay/ClaimTimerView.js
@@ -1,6 +1,6 @@
-import React, {useEffect, useRef, useState} from 'react';
-import {StyleSheet, Text, View} from 'react-native';
-import {CACHE_CLAIM_TIMER} from '../../utils/geoMath';
+import { useEffect, useRef, useState } from "react";
+import { StyleSheet, Text, View } from "react-native";
+import { CACHE_CLAIM_TIMER } from "../../utils/geoMath";
const ClaimTimerView = ({
cache,
@@ -8,7 +8,7 @@ const ClaimTimerView = ({
isClaiming,
showClaimedPopup,
claimDurationSeconds = CACHE_CLAIM_TIMER,
- claimedPopupMessage = 'Cache Claimed!',
+ claimedPopupMessage = "Cache Claimed!",
autoClaim = true,
}) => {
const [timeLeft, setTimeLeft] = useState(claimDurationSeconds);
@@ -75,30 +75,30 @@ const ClaimTimerView = ({
const styles = StyleSheet.create({
overlay: {
- alignSelf: 'center',
- backgroundColor: 'rgba(0, 0, 0, 0.75)',
+ alignSelf: "center",
+ backgroundColor: "rgba(0, 0, 0, 0.75)",
paddingHorizontal: 16,
paddingVertical: 12,
borderRadius: 10,
marginTop: 8,
},
text: {
- color: '#cdd6f4',
+ color: "#cdd6f4",
fontSize: 16,
- fontWeight: '700',
+ fontWeight: "700",
},
claimedOverlay: {
- alignSelf: 'center',
- backgroundColor: '#a6e3a1',
+ alignSelf: "center",
+ backgroundColor: "#a6e3a1",
paddingHorizontal: 20,
paddingVertical: 14,
borderRadius: 10,
marginTop: 8,
},
claimedText: {
- color: '#1e1e2e',
+ color: "#1e1e2e",
fontSize: 18,
- fontWeight: '700',
+ fontWeight: "700",
},
});
diff --git a/src/components/gameplay/PlayerMapView.js b/src/components/gameplay/PlayerMapView.js
index 5ff1f4b..6cff766 100644
--- a/src/components/gameplay/PlayerMapView.js
+++ b/src/components/gameplay/PlayerMapView.js
@@ -1,45 +1,47 @@
-import React from 'react';
-import MapView, {Marker, Polygon} from 'react-native-maps';
-import {getFovCone} from '../../utils/geoMath';
+import MapView, { Marker, Polygon } from "react-native-maps";
+import { getFovCone } from "../../utils/geoMath";
-const PlayerMapView = ({userLocation, visibleCaches, heading}) => {
- if (!userLocation) return null;
+const PlayerMapView = ({ userLocation, visibleCaches, heading }) => {
+ if (!userLocation) return null;
- const coneCoords = (heading !== null && heading !== undefined)
- ? getFovCone(userLocation, heading)
- : null;
+ const coneCoords =
+ heading !== null && heading !== undefined
+ ? getFovCone(userLocation, heading)
+ : null;
- return (
-
- {(visibleCaches || []).map((cache) => (
-
- ))}
- {coneCoords && (
-
- )}
-
- );
+ return (
+
+ {(visibleCaches || []).map((cache) => (
+
+ ))}
+ {coneCoords && (
+
+ )}
+
+ );
};
export default PlayerMapView;
-
-
diff --git a/src/components/screens/DepartmentSettingsScreen.js b/src/components/screens/DepartmentSettingsScreen.js
index 1d6824a..8b43102 100644
--- a/src/components/screens/DepartmentSettingsScreen.js
+++ b/src/components/screens/DepartmentSettingsScreen.js
@@ -1,29 +1,38 @@
-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';
+import { 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 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 [deptName, setDeptName] = useState("");
+ const [joinCode, setJoinCode] = useState("");
const [teamsEnabled, setTeamsEnabled] = useState(false);
- const [cacheTriggerMeters, setCacheTriggerMeters] = useState('20');
+ const [cacheTriggerMeters, setCacheTriggerMeters] = useState("20");
const [cacheCount, setCacheCount] = useState(0);
-// Handlers -------------------
+ // Handlers -------------------
useEffect(() => {
const load = async () => {
const sg = await getSubgroup(SGid);
if (sg) {
- setDeptName(sg.SubGroupName || '');
- setJoinCode(sg.JoinCode || '');
+ setDeptName(sg.SubGroupName || "");
+ setJoinCode(sg.JoinCode || "");
setTeamsEnabled(Boolean(sg.TeamsEnabled));
setCacheTriggerMeters(String(sg.CacheTriggerMeters || 20));
}
@@ -35,190 +44,208 @@ const DepartmentSettingsScreen = ({route, navigation}) => {
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 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}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ 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}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
};
const styles = StyleSheet.create({
- center: {justifyContent: 'center', alignItems: 'center'},
- formContainer: {paddingBottom: 20},
- sectionTitle: {fontSize: 20, fontWeight: '700', color: '#cdd6f4', marginBottom: 16},
- label: {fontSize: 14, fontWeight: '600', color: '#bac2de', marginBottom: 6, marginTop: 14},
- input: {
- borderWidth: 1,
- borderColor: '#45475a',
- borderRadius: 8,
- paddingHorizontal: 12,
- paddingVertical: 10,
- fontSize: 16,
- color: '#cdd6f4',
- backgroundColor: '#313244',
- },
- codeSection: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- marginTop: 16,
- paddingVertical: 12,
- paddingHorizontal: 12,
- backgroundColor: '#313244',
- borderRadius: 8,
- },
- codeLabel: {fontSize: 14, fontWeight: '600', color: '#bac2de'},
- codeValue: {fontSize: 16, fontWeight: '700', color: '#bd93f9', letterSpacing: 2},
- toggleRow: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- marginTop: 16,
- paddingVertical: 10,
- borderBottomWidth: 1,
- borderBottomColor: '#45475a',
- },
- toggleLabel: {fontSize: 14, fontWeight: '600', color: '#bac2de'},
- infoRow: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- marginTop: 16,
- paddingVertical: 12,
- paddingHorizontal: 12,
- backgroundColor: '#313244',
- borderRadius: 8,
- },
- infoLabel: {fontSize: 14, fontWeight: '600', color: '#bac2de'},
- infoValue: {fontSize: 16, fontWeight: '700', color: '#cdd6f4'},
- saveWrap: {marginTop: 24},
- saveButton: {backgroundColor: '#a6e3a1', borderColor: '#a6e3a1'},
- saveLabel: {color: '#1e1e2e', fontWeight: '600'},
- mapWrap: {marginTop: 16},
- mapButton: {backgroundColor: '#bd93f9', borderColor: '#bd93f9'},
- mapLabel: {color: '#1e1e2e', fontWeight: '600'},
- resetWrap: {marginTop: 30},
- resetButton: {backgroundColor: '#86efac', borderColor: '#86efac'},
- resetLabel: {color: '#1e1e2e', fontWeight: '600'},
+ center: { justifyContent: "center", alignItems: "center" },
+ formContainer: { paddingBottom: 20 },
+ sectionTitle: {
+ fontSize: 20,
+ fontWeight: "700",
+ color: "#cdd6f4",
+ marginBottom: 16,
+ },
+ label: {
+ fontSize: 14,
+ fontWeight: "600",
+ color: "#bac2de",
+ marginBottom: 6,
+ marginTop: 14,
+ },
+ input: {
+ borderWidth: 1,
+ borderColor: "#45475a",
+ borderRadius: 8,
+ paddingHorizontal: 12,
+ paddingVertical: 10,
+ fontSize: 16,
+ color: "#cdd6f4",
+ backgroundColor: "#313244",
+ },
+ codeSection: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "center",
+ marginTop: 16,
+ paddingVertical: 12,
+ paddingHorizontal: 12,
+ backgroundColor: "#313244",
+ borderRadius: 8,
+ },
+ codeLabel: { fontSize: 14, fontWeight: "600", color: "#bac2de" },
+ codeValue: {
+ fontSize: 16,
+ fontWeight: "700",
+ color: "#bd93f9",
+ letterSpacing: 2,
+ },
+ toggleRow: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "center",
+ marginTop: 16,
+ paddingVertical: 10,
+ borderBottomWidth: 1,
+ borderBottomColor: "#45475a",
+ },
+ toggleLabel: { fontSize: 14, fontWeight: "600", color: "#bac2de" },
+ infoRow: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "center",
+ marginTop: 16,
+ paddingVertical: 12,
+ paddingHorizontal: 12,
+ backgroundColor: "#313244",
+ borderRadius: 8,
+ },
+ infoLabel: { fontSize: 14, fontWeight: "600", color: "#bac2de" },
+ infoValue: { fontSize: 16, fontWeight: "700", color: "#cdd6f4" },
+ saveWrap: { marginTop: 24 },
+ saveButton: { backgroundColor: "#a6e3a1", borderColor: "#a6e3a1" },
+ saveLabel: { color: "#1e1e2e", fontWeight: "600" },
+ mapWrap: { marginTop: 16 },
+ mapButton: { backgroundColor: "#bd93f9", borderColor: "#bd93f9" },
+ mapLabel: { color: "#1e1e2e", fontWeight: "600" },
+ resetWrap: { marginTop: 30 },
+ resetButton: { backgroundColor: "#86efac", borderColor: "#86efac" },
+ resetLabel: { color: "#1e1e2e", fontWeight: "600" },
});
export default DepartmentSettingsScreen;
-
diff --git a/src/components/screens/GameSettingsScreen.js b/src/components/screens/GameSettingsScreen.js
index ad6593a..766e0d9 100644
--- a/src/components/screens/GameSettingsScreen.js
+++ b/src/components/screens/GameSettingsScreen.js
@@ -1,15 +1,36 @@
-import React, {useCallback, useEffect, useRef, useState} from 'react';
-import {ActivityIndicator, Alert, ScrollView, StyleSheet, Switch, Text, TextInput, View} from 'react-native';
-import {useFocusEffect} from '@react-navigation/native';
-import Screen from '../layout/Screen';
-import Card from '../UI/Card';
-import {Button, ButtonTray} from '../UI/Button';
-import useGameHook from '../../hooks/useGameHook';
-import {getSession, setSessionTeamsEnabled} from '../../hooks/SessionStore';
+import { useCallback, useEffect, useRef, useState } from "react";
+import {
+ ActivityIndicator,
+ Alert,
+ ScrollView,
+ StyleSheet,
+ Switch,
+ Text,
+ TextInput,
+ View,
+} from "react-native";
+import { useFocusEffect } from "@react-navigation/native";
+import Screen from "../layout/Screen";
+import Card from "../UI/Card";
+import { Button, ButtonTray } from "../UI/Button";
+import useGameHook from "../../hooks/useGameHook";
+import { getSession, setSessionTeamsEnabled } from "../../hooks/SessionStore";
-const GameSettingsScreen = ({navigation}) => {
+const GameSettingsScreen = ({ navigation }) => {
const session = getSession();
- const {getLobby, updateGroup, getSubgroups, createSubgroup, deleteSubgroup, resetGame, getAdminWaitlist, approveAdmin, rejectAdmin, getUser, disbandTeams} = useGameHook();
+ const {
+ getLobby,
+ updateGroup,
+ getSubgroups,
+ createSubgroup,
+ deleteSubgroup,
+ resetGame,
+ getAdminWaitlist,
+ approveAdmin,
+ rejectAdmin,
+ getUser,
+ disbandTeams,
+ } = useGameHook();
const gameApiRef = useRef({
getLobby,
getSubgroups,
@@ -28,27 +49,34 @@ const GameSettingsScreen = ({navigation}) => {
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
- const [groupName, setGroupName] = useState('');
- const [businessName, setBusinessName] = useState('');
+ const [groupName, setGroupName] = useState("");
+ const [businessName, setBusinessName] = useState("");
const [teamsEnabled, setTeamsEnabled] = useState(session.teamsEnabled);
- const [serverTeamsEnabled, setServerTeamsEnabled] = useState(session.teamsEnabled);
- const [cacheTriggerMeters, setCacheTriggerMeters] = useState('20');
- const [adminJoinCode, setAdminJoinCode] = useState('');
- const [orgJoinCode, setOrgJoinCode] = useState('');
- const [memberJoinCode, setMemberJoinCode] = useState('');
+ const [serverTeamsEnabled, setServerTeamsEnabled] = useState(
+ session.teamsEnabled,
+ );
+ const [cacheTriggerMeters, setCacheTriggerMeters] = useState("20");
+ const [adminJoinCode, setAdminJoinCode] = useState("");
+ const [orgJoinCode, setOrgJoinCode] = useState("");
+ const [memberJoinCode, setMemberJoinCode] = useState("");
const [waitlist, setWaitlist] = useState([]);
const [waitlistNames, setWaitlistNames] = useState({});
- const [isBusinessGroup, setIsBusinessGroup] = useState(Boolean(session.isBusiness));
+ const [isBusinessGroup, setIsBusinessGroup] = useState(
+ Boolean(session.isBusiness),
+ );
const [subgroupList, setSubgroupList] = useState([]);
- const [newDeptName, setNewDeptName] = useState('');
+ const [newDeptName, setNewDeptName] = useState("");
-// Handlers -------------------
+ // Handlers -------------------
- useEffect(() => () => {
- isMountedRef.current = false;
- }, []);
+ useEffect(
+ () => () => {
+ isMountedRef.current = false;
+ },
+ [],
+ );
- const refreshSettings = useCallback(async ({showLoader = false} = {}) => {
+ const refreshSettings = useCallback(async ({ showLoader = false } = {}) => {
const activeSession = getSession();
if (!activeSession.currentGid) {
if (isMountedRef.current) setLoading(false);
@@ -61,496 +89,592 @@ const GameSettingsScreen = ({navigation}) => {
if (showLoader && isMountedRef.current) setLoading(true);
- try {
- const [group, sgs, waitlistRows] = await Promise.all([
- gameApiRef.current.getLobby(activeSession.currentGid),
- gameApiRef.current.getSubgroups(activeSession.currentGid),
- gameApiRef.current.getAdminWaitlist(activeSession.currentGid),
- ]);
+ try {
+ const [group, sgs, waitlistRows] = await Promise.all([
+ gameApiRef.current.getLobby(activeSession.currentGid),
+ gameApiRef.current.getSubgroups(activeSession.currentGid),
+ gameApiRef.current.getAdminWaitlist(activeSession.currentGid),
+ ]);
- if (!isMountedRef.current) return;
+ if (!isMountedRef.current) return;
- if (group) {
- setGroupName(group.GroupName || '');
- setBusinessName(group.BusinessOrSchoolName || '');
- // Organisation mode
- setIsBusinessGroup(Boolean(group.BusinessOrSchoolName));
+ if (group) {
+ setGroupName(group.GroupName || "");
+ setBusinessName(group.BusinessOrSchoolName || "");
+ // Organisation mode
+ setIsBusinessGroup(Boolean(group.BusinessOrSchoolName));
- const serverTeams = Boolean(group.TeamsEnabled);
- setTeamsEnabled(serverTeams);
- setServerTeamsEnabled(serverTeams);
- setSessionTeamsEnabled(serverTeams);
+ const serverTeams = Boolean(group.TeamsEnabled);
+ setTeamsEnabled(serverTeams);
+ setServerTeamsEnabled(serverTeams);
+ setSessionTeamsEnabled(serverTeams);
- setCacheTriggerMeters(String(group.CacheTriggerMeters || 20));
- setAdminJoinCode(group.AdminJoinCode || '');
- setOrgJoinCode(group.OrgJoinCode || '');
- }
+ setCacheTriggerMeters(String(group.CacheTriggerMeters || 20));
+ setAdminJoinCode(group.AdminJoinCode || "");
+ setOrgJoinCode(group.OrgJoinCode || "");
+ }
- setSubgroupList(sgs || []);
- const memberSg = (sgs || []).find((sg) => !sg.IsAdminGroup);
- setMemberJoinCode(memberSg?.JoinCode || '');
+ setSubgroupList(sgs || []);
+ const memberSg = (sgs || []).find((sg) => !sg.IsAdminGroup);
+ setMemberJoinCode(memberSg?.JoinCode || "");
- setWaitlist(waitlistRows || []);
- const names = {};
- for (const entry of (waitlistRows || [])) {
- const user = await gameApiRef.current.getUser(entry.Uid);
- if (user) names[entry.Uid] = user.username;
- }
- if (!isMountedRef.current) return;
- setWaitlistNames(names);
- hasHydratedRef.current = true;
- } finally {
- if (isMountedRef.current && (showLoader || !hasHydratedRef.current)) {
- setLoading(false);
- }
- isRefreshingRef.current = false;
- }
- }, []);
-
- useFocusEffect(
- useCallback(() => {
- refreshSettings({showLoader: !hasHydratedRef.current});
- }, [refreshSettings]),
- );
-
- // --- Individual save ---
- const saveSettings = async () => {
- setSaving(true);
- const result = await updateGroup(session.currentGid, {
- GroupName: groupName.trim(),
- TeamsEnabled: teamsEnabled,
- CacheTriggerMeters: parseInt(cacheTriggerMeters) || 20,
- });
- if (result) {
- if (serverTeamsEnabled && !teamsEnabled) {
- await disbandTeams(session.currentGid);
- }
- setSessionTeamsEnabled(teamsEnabled);
- setServerTeamsEnabled(teamsEnabled);
- }
- setSaving(false);
- };
-
- const handleSave = async () => {
- if (!session.currentGid) return;
- // Business save — only save business name
- if (isBusinessGroup) {
- setSaving(true);
- await updateGroup(session.currentGid, {
- BusinessOrSchoolName: businessName.trim(),
- GroupName: businessName.trim(),
- });
- setSaving(false);
- return;
- }
- // Individual save — check teams toggle
- if (serverTeamsEnabled && !teamsEnabled) {
- Alert.alert(
- 'Disable Teams',
- 'This will disband all existing teams and remove all players from their teams. Continue?',
- [
- {text: 'Cancel', style: 'cancel'},
- {text: 'Disable', style: 'destructive', onPress: saveSettings},
- ],
- );
- return;
- }
- await saveSettings();
- };
-
- const handleResetGame = () => {
- Alert.alert('Reset Entire Game', 'This will remove ALL saved caches and reset progress for every player. Are you sure?', [
- {text: 'Cancel', style: 'cancel'},
- {
- text: 'Reset',
- style: 'destructive',
- onPress: async () => {
- await resetGame(session.currentGid);
- Alert.alert('Done', 'All caches and player progress have been reset.');
- },
- },
- ]);
- };
-
- const handleApproveAdmin = async (entry) => {
- await approveAdmin(entry.id);
- await refreshSettings();
- };
-
- const handleRejectAdmin = async (entry) => {
- await rejectAdmin(entry.id);
- await refreshSettings();
- };
-
- // Business — create a new department (subgroup)
- const handleCreateDepartment = async () => {
- const deptCount = subgroupList.filter((sg) => !sg.IsAdminGroup).length;
- const name = newDeptName.trim() || `Department ${deptCount + 1}`;
- if (!session.currentGid) return;
- await createSubgroup({
- SubGroupName: name,
- Gid: session.currentGid,
- IsAdminGroup: false,
- CacheTriggerMeters: 20,
- });
- setNewDeptName('');
- await refreshSettings();
- };
-
- // Business — delete a department
- const handleDeleteDepartment = (sg) => {
- Alert.alert('Delete Department', `Delete "${sg.SubGroupName}" and all its data?`, [
- {text: 'Cancel', style: 'cancel'},
- {
- text: 'Delete',
- style: 'destructive',
- onPress: async () => {
- await deleteSubgroup(sg.SGid);
- await refreshSettings();
- },
- },
- ]);
- };
-
- // Business — navigate to department settings
- const handleEnterDepartment = (sg) => {
- navigation.navigate('DepartmentSettingsScreen', {
- SGid: sg.SGid,
- Gid: session.currentGid,
- SubGroupName: sg.SubGroupName,
- });
- };
-
-// View -----------------------
-
- if (loading) {
- return (
-
-
-
- );
+ setWaitlist(waitlistRows || []);
+ const names = {};
+ for (const entry of waitlistRows || []) {
+ const user = await gameApiRef.current.getUser(entry.Uid);
+ if (user) names[entry.Uid] = user.username;
+ }
+ if (!isMountedRef.current) return;
+ setWaitlistNames(names);
+ hasHydratedRef.current = true;
+ } finally {
+ if (isMountedRef.current && (showLoader || !hasHydratedRef.current)) {
+ setLoading(false);
+ }
+ isRefreshingRef.current = false;
}
+ }, []);
- if (!session.currentGid) {
- return (
-
- Create or join a game first.
-
- );
+ useFocusEffect(
+ useCallback(() => {
+ refreshSettings({ showLoader: !hasHydratedRef.current });
+ }, [refreshSettings]),
+ );
+
+ // --- Individual save ---
+ const saveSettings = async () => {
+ setSaving(true);
+ const result = await updateGroup(session.currentGid, {
+ GroupName: groupName.trim(),
+ TeamsEnabled: teamsEnabled,
+ CacheTriggerMeters: parseInt(cacheTriggerMeters) || 20,
+ });
+ if (result) {
+ if (serverTeamsEnabled && !teamsEnabled) {
+ await disbandTeams(session.currentGid);
+ }
+ setSessionTeamsEnabled(teamsEnabled);
+ setServerTeamsEnabled(teamsEnabled);
}
+ setSaving(false);
+ };
- // Filter subgroups for the department list (non-admin only)
- const departments = subgroupList.filter((sg) => !sg.IsAdminGroup);
-
- // ===== Business Admin View =====
+ const handleSave = async () => {
+ if (!session.currentGid) return;
+ // Business save — only save business name
if (isBusinessGroup) {
- return (
-
-
- Game Configuration :
-
- Organisation Name:
-
-
-
- Admin join code:
- {adminJoinCode || '—'}
-
-
-
- Org join code:
- {orgJoinCode || '—'}
-
-
-
-
-
-
-
-
- {/* Departments List */}
-
- Departments:
- {departments.map((sg, index) => (
-
- Department {index + 1}:
-
-
- Department Name
- {sg.SubGroupName}
-
-
-
-
-
-
- Department Join code
- {sg.JoinCode || '—'}
-
-
-
-
-
- ))}
- {departments.length === 0 && (
- No departments yet.
- )}
-
-
-
-
-
-
- {/* Admin Waitlist */}
- {waitlist.length > 0 && (
-
- Admin Waitlist
- {waitlist.map((entry) => (
-
-
-
- {waitlistNames[entry.Uid] || `User ${entry.Uid}`}
-
-
-
-
-
- ))}
-
- )}
-
- {/* Reset Entire Game */}
-
-
-
-
-
- );
+ setSaving(true);
+ await updateGroup(session.currentGid, {
+ BusinessOrSchoolName: businessName.trim(),
+ GroupName: businessName.trim(),
+ });
+ setSaving(false);
+ return;
}
+ // Individual save — check teams toggle
+ if (serverTeamsEnabled && !teamsEnabled) {
+ Alert.alert(
+ "Disable Teams",
+ "This will disband all existing teams and remove all players from their teams. Continue?",
+ [
+ { text: "Cancel", style: "cancel" },
+ { text: "Disable", style: "destructive", onPress: saveSettings },
+ ],
+ );
+ return;
+ }
+ await saveSettings();
+ };
- // ===== Individual Admin View =====
- return (
-
-
- Game Configuration
-
- Game Name
-
-
-
- Teams Enabled
-
-
-
- Cache Claim Distance (metres)
-
-
-
- Admin Join Code
- {adminJoinCode || '—'}
-
-
-
- Game Join Code
- {memberJoinCode || '—'}
-
-
-
-
-
-
-
-
- {/* Admin Waitlist */}
- {waitlist.length > 0 && (
-
- Admin Waitlist
- {waitlist.map((entry) => (
-
-
-
- {waitlistNames[entry.Uid] || `User ${entry.Uid}`}
-
-
- handleApproveAdmin(entry)}
- styleButton={styles.approveButton}
- styleLabel={styles.approveBtnLabel}
- />
- handleRejectAdmin(entry)}
- styleButton={styles.rejectButton}
- styleLabel={styles.rejectBtnLabel}
- />
-
-
-
- ))}
-
- )}
-
- {/* Reset Entire Game */}
-
-
-
-
-
+ const handleResetGame = () => {
+ Alert.alert(
+ "Reset Entire Game",
+ "This will remove ALL saved caches and reset progress for every player. Are you sure?",
+ [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Reset",
+ style: "destructive",
+ onPress: async () => {
+ await resetGame(session.currentGid);
+ Alert.alert(
+ "Done",
+ "All caches and player progress have been reset.",
+ );
+ },
+ },
+ ],
);
+ };
+
+ const handleApproveAdmin = async (entry) => {
+ await approveAdmin(entry.id);
+ await refreshSettings();
+ };
+
+ const handleRejectAdmin = async (entry) => {
+ await rejectAdmin(entry.id);
+ await refreshSettings();
+ };
+
+ // Business — create a new department (subgroup)
+ const handleCreateDepartment = async () => {
+ const deptCount = subgroupList.filter((sg) => !sg.IsAdminGroup).length;
+ const name = newDeptName.trim() || `Department ${deptCount + 1}`;
+ if (!session.currentGid) return;
+ await createSubgroup({
+ SubGroupName: name,
+ Gid: session.currentGid,
+ IsAdminGroup: false,
+ CacheTriggerMeters: 20,
+ });
+ setNewDeptName("");
+ await refreshSettings();
+ };
+
+ // Business — delete a department
+ const handleDeleteDepartment = (sg) => {
+ Alert.alert(
+ "Delete Department",
+ `Delete "${sg.SubGroupName}" and all its data?`,
+ [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Delete",
+ style: "destructive",
+ onPress: async () => {
+ await deleteSubgroup(sg.SGid);
+ await refreshSettings();
+ },
+ },
+ ],
+ );
+ };
+
+ // Business — navigate to department settings
+ const handleEnterDepartment = (sg) => {
+ navigation.navigate("DepartmentSettingsScreen", {
+ SGid: sg.SGid,
+ Gid: session.currentGid,
+ SubGroupName: sg.SubGroupName,
+ });
+ };
+
+ // View -----------------------
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (!session.currentGid) {
+ return (
+
+ Create or join a game first.
+
+ );
+ }
+
+ // Filter subgroups for the department list (non-admin only)
+ const departments = subgroupList.filter((sg) => !sg.IsAdminGroup);
+
+ // ===== Business Admin View =====
+ if (isBusinessGroup) {
+ return (
+
+
+ Game Configuration :
+
+ Organisation Name:
+
+
+
+ Admin join code:
+ {adminJoinCode || "—"}
+
+
+
+ Org join code:
+ {orgJoinCode || "—"}
+
+
+
+
+
+
+
+
+ {/* Departments List */}
+
+ Departments:
+ {departments.map((sg, index) => (
+
+
+ Department {index + 1}:
+
+
+
+ Department Name
+ {sg.SubGroupName}
+
+
+ handleEnterDepartment(sg)}
+ styleButton={styles.deptOpenButton}
+ styleLabel={styles.deptBtnLabel}
+ />
+
+
+
+
+
+ Department Join code
+
+ {sg.JoinCode || "—"}
+
+
+ handleDeleteDepartment(sg)}
+ styleButton={styles.deptDeleteButton}
+ styleLabel={styles.deptBtnLabel}
+ />
+
+
+
+ ))}
+ {departments.length === 0 && (
+ No departments yet.
+ )}
+
+
+
+
+
+
+ {/* Admin Waitlist */}
+ {waitlist.length > 0 && (
+
+ Admin Waitlist
+ {waitlist.map((entry) => (
+
+
+
+ {waitlistNames[entry.Uid] || `User ${entry.Uid}`}
+
+
+ handleApproveAdmin(entry)}
+ styleButton={styles.approveButton}
+ styleLabel={styles.approveBtnLabel}
+ />
+ handleRejectAdmin(entry)}
+ styleButton={styles.rejectButton}
+ styleLabel={styles.rejectBtnLabel}
+ />
+
+
+
+ ))}
+
+ )}
+
+ {/* Reset Entire Game */}
+
+
+
+
+
+ );
+ }
+
+ // ===== Individual Admin View =====
+ return (
+
+
+ Game Configuration
+
+ Game Name
+
+
+
+ Teams Enabled
+
+
+
+ Cache Claim Distance (metres)
+
+
+
+ Admin Join Code
+ {adminJoinCode || "—"}
+
+
+
+ Game Join Code
+ {memberJoinCode || "—"}
+
+
+
+
+
+
+
+
+ {/* Admin Waitlist */}
+ {waitlist.length > 0 && (
+
+ Admin Waitlist
+ {waitlist.map((entry) => (
+
+
+
+ {waitlistNames[entry.Uid] || `User ${entry.Uid}`}
+
+
+ handleApproveAdmin(entry)}
+ styleButton={styles.approveButton}
+ styleLabel={styles.approveBtnLabel}
+ />
+ handleRejectAdmin(entry)}
+ styleButton={styles.rejectButton}
+ styleLabel={styles.rejectBtnLabel}
+ />
+
+
+
+ ))}
+
+ )}
+
+ {/* Reset Entire Game */}
+
+
+
+
+
+ );
};
const styles = StyleSheet.create({
- center: {justifyContent: 'center', alignItems: 'center'},
- formContainer: {paddingBottom: 20},
- sectionTitle: {fontSize: 20, fontWeight: '700', color: '#cdd6f4', marginBottom: 16},
- label: {fontSize: 14, fontWeight: '600', color: '#bac2de', marginBottom: 6, marginTop: 14},
- body: {color: '#bac2de', fontSize: 15},
- input: {
- borderWidth: 1,
- borderColor: '#45475a',
- borderRadius: 8,
- paddingHorizontal: 12,
- paddingVertical: 10,
- fontSize: 16,
- color: '#cdd6f4',
- backgroundColor: '#313244',
- },
- toggleRow: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- marginTop: 16,
- paddingVertical: 10,
- borderBottomWidth: 1,
- borderBottomColor: '#45475a',
- },
- toggleLabel: {fontSize: 14, fontWeight: '600', color: '#bac2de'},
- codeSection: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- marginTop: 16,
- paddingVertical: 12,
- paddingHorizontal: 12,
- backgroundColor: '#313244',
- borderRadius: 8,
- },
- codeLabel: {fontSize: 14, fontWeight: '600', color: '#bac2de'},
- codeValue: {fontSize: 16, fontWeight: '700', color: '#bd93f9', letterSpacing: 2},
- saveWrap: {marginTop: 24},
- saveButton: {backgroundColor: '#a6e3a1', borderColor: '#a6e3a1'},
- saveLabel: {color: '#1e1e2e', fontWeight: '600'},
- deptSection: {marginTop: 30},
- deptIndexLabel: {fontSize: 14, fontWeight: '700', color: '#bac2de', marginBottom: 8},
- deptCardRow: {flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8},
- deptCardInfo: {flex: 1, paddingRight: 10},
- deptCardActions: {flexDirection: 'row', gap: 6, flex: 0},
- deptInfoLabel: {fontSize: 12, fontWeight: '600', color: '#6c7086', marginBottom: 2},
- deptName: {fontSize: 15, fontWeight: '700', color: '#cdd6f4'},
- deptCode: {fontSize: 13, fontWeight: '600', color: '#bd93f9', marginTop: 2, letterSpacing: 1},
- deptDeleteButton: {backgroundColor: '#D92800', borderColor: '#D92800', minHeight: 36, flex: 0, paddingHorizontal: 10},
- deptOpenButton: {backgroundColor: '#bd93f9', borderColor: '#bd93f9', minHeight: 36, flex: 0, paddingHorizontal: 10},
- deptBtnLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 13},
- createDeptRow: {flexDirection: 'row', gap: 10, marginTop: 12, alignItems: 'center'},
- createDeptButton: {backgroundColor: '#a6e3a1', borderColor: '#a6e3a1', flex: 0, paddingHorizontal: 16},
- createDeptLabel: {color: '#1e1e2e', fontWeight: '600'},
- emptyText: {color: '#6c7086', textAlign: 'center', marginTop: 10, fontSize: 14},
- waitlistSection: {marginTop: 30},
- waitlistRow: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- },
- waitlistName: {fontSize: 15, fontWeight: '600', color: '#cdd6f4', flex: 1},
- waitlistActions: {flexDirection: 'row', gap: 6},
- approveButton: {backgroundColor: '#a6e3a1', borderColor: '#a6e3a1', minHeight: 36, flex: 0, paddingHorizontal: 10},
- approveBtnLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 13},
- rejectButton: {backgroundColor: '#D92800', borderColor: '#D92800', minHeight: 36, flex: 0, paddingHorizontal: 10},
- rejectBtnLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 13},
- resetSection: {marginTop: 30},
- resetButton: {backgroundColor: '#D92800', borderColor: '#D92800'},
- resetLabel: {color: '#ffffff', fontWeight: '600'},
+ center: { justifyContent: "center", alignItems: "center" },
+ formContainer: { paddingBottom: 20 },
+ sectionTitle: {
+ fontSize: 20,
+ fontWeight: "700",
+ color: "#cdd6f4",
+ marginBottom: 16,
+ },
+ label: {
+ fontSize: 14,
+ fontWeight: "600",
+ color: "#bac2de",
+ marginBottom: 6,
+ marginTop: 14,
+ },
+ body: { color: "#bac2de", fontSize: 15 },
+ input: {
+ borderWidth: 1,
+ borderColor: "#45475a",
+ borderRadius: 8,
+ paddingHorizontal: 12,
+ paddingVertical: 10,
+ fontSize: 16,
+ color: "#cdd6f4",
+ backgroundColor: "#313244",
+ },
+ toggleRow: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "center",
+ marginTop: 16,
+ paddingVertical: 10,
+ borderBottomWidth: 1,
+ borderBottomColor: "#45475a",
+ },
+ toggleLabel: { fontSize: 14, fontWeight: "600", color: "#bac2de" },
+ codeSection: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "center",
+ marginTop: 16,
+ paddingVertical: 12,
+ paddingHorizontal: 12,
+ backgroundColor: "#313244",
+ borderRadius: 8,
+ },
+ codeLabel: { fontSize: 14, fontWeight: "600", color: "#bac2de" },
+ codeValue: {
+ fontSize: 16,
+ fontWeight: "700",
+ color: "#bd93f9",
+ letterSpacing: 2,
+ },
+ saveWrap: { marginTop: 24 },
+ saveButton: { backgroundColor: "#a6e3a1", borderColor: "#a6e3a1" },
+ saveLabel: { color: "#1e1e2e", fontWeight: "600" },
+ deptSection: { marginTop: 30 },
+ deptIndexLabel: {
+ fontSize: 14,
+ fontWeight: "700",
+ color: "#bac2de",
+ marginBottom: 8,
+ },
+ deptCardRow: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "center",
+ marginBottom: 8,
+ },
+ deptCardInfo: { flex: 1, paddingRight: 10 },
+ deptCardActions: { flexDirection: "row", gap: 6, flex: 0 },
+ deptInfoLabel: {
+ fontSize: 12,
+ fontWeight: "600",
+ color: "#6c7086",
+ marginBottom: 2,
+ },
+ deptName: { fontSize: 15, fontWeight: "700", color: "#cdd6f4" },
+ deptCode: {
+ fontSize: 13,
+ fontWeight: "600",
+ color: "#bd93f9",
+ marginTop: 2,
+ letterSpacing: 1,
+ },
+ deptDeleteButton: {
+ backgroundColor: "#D92800",
+ borderColor: "#D92800",
+ minHeight: 36,
+ flex: 0,
+ paddingHorizontal: 10,
+ },
+ deptOpenButton: {
+ backgroundColor: "#bd93f9",
+ borderColor: "#bd93f9",
+ minHeight: 36,
+ flex: 0,
+ paddingHorizontal: 10,
+ },
+ deptBtnLabel: { color: "#1e1e2e", fontWeight: "600", fontSize: 13 },
+ createDeptRow: {
+ flexDirection: "row",
+ gap: 10,
+ marginTop: 12,
+ alignItems: "center",
+ },
+ createDeptButton: {
+ backgroundColor: "#a6e3a1",
+ borderColor: "#a6e3a1",
+ flex: 0,
+ paddingHorizontal: 16,
+ },
+ createDeptLabel: { color: "#1e1e2e", fontWeight: "600" },
+ emptyText: {
+ color: "#6c7086",
+ textAlign: "center",
+ marginTop: 10,
+ fontSize: 14,
+ },
+ waitlistSection: { marginTop: 30 },
+ waitlistRow: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "center",
+ },
+ waitlistName: { fontSize: 15, fontWeight: "600", color: "#cdd6f4", flex: 1 },
+ waitlistActions: { flexDirection: "row", gap: 6 },
+ approveButton: {
+ backgroundColor: "#a6e3a1",
+ borderColor: "#a6e3a1",
+ minHeight: 36,
+ flex: 0,
+ paddingHorizontal: 10,
+ },
+ approveBtnLabel: { color: "#1e1e2e", fontWeight: "600", fontSize: 13 },
+ rejectButton: {
+ backgroundColor: "#D92800",
+ borderColor: "#D92800",
+ minHeight: 36,
+ flex: 0,
+ paddingHorizontal: 10,
+ },
+ rejectBtnLabel: { color: "#1e1e2e", fontWeight: "600", fontSize: 13 },
+ resetSection: { marginTop: 30 },
+ resetButton: { backgroundColor: "#D92800", borderColor: "#D92800" },
+ resetLabel: { color: "#ffffff", fontWeight: "600" },
});
export default GameSettingsScreen;
-
diff --git a/src/components/screens/LeaderboardScreen.js b/src/components/screens/LeaderboardScreen.js
index 670da31..1cad456 100644
--- a/src/components/screens/LeaderboardScreen.js
+++ b/src/components/screens/LeaderboardScreen.js
@@ -1,474 +1,661 @@
-import React, {useCallback, useEffect, useState} from 'react';
-import {ActivityIndicator, Alert, Pressable, ScrollView, StyleSheet, Text, View} from 'react-native';
-import Screen from '../layout/Screen';
-import Card from '../UI/Card';
-import {Button} from '../UI/Button';
-import PendingApprovalView from '../UI/PendingApprovalView';
-import useGameHook from '../../hooks/useGameHook';
-import {getSession} from '../../hooks/SessionStore';
+import { useCallback, useEffect, useState } from "react";
+import {
+ ActivityIndicator,
+ Alert,
+ Pressable,
+ ScrollView,
+ StyleSheet,
+ Text,
+ View,
+} from "react-native";
+import Screen from "../layout/Screen";
+import Card from "../UI/Card";
+import { Button } from "../UI/Button";
+import PendingApprovalView from "../UI/PendingApprovalView";
+import useGameHook from "../../hooks/useGameHook";
+import { getSession } from "../../hooks/SessionStore";
-const LeaderboardScreen = ({navigation, route}) => {
-// Initialisation ------------
+const LeaderboardScreen = ({ navigation, route }) => {
+ // Initialisation ------------
- const session = getSession();
- const isAdmin = session.isAcceptedAdmin;
- const isBusiness = Boolean(session.isBusiness);
- const selectedDepartment = route?.params?.department || null;
- const effectiveSGid = isBusiness
- ? ((isAdmin && selectedDepartment?.SGid) ? selectedDepartment.SGid : (isAdmin ? null : session.currentSGid))
- : null;
- const {getLobby, getSubgroup, getTeams, getTeamMembers, getGroupMembers, getCaches, getUser, deleteTeam, resetPlayerProgress, resetTeamProgress, getSubgroups} = useGameHook();
+ const session = getSession();
+ const isAdmin = session.isAcceptedAdmin;
+ const isBusiness = Boolean(session.isBusiness);
+ const selectedDepartment = route?.params?.department || null;
+ const effectiveSGid = isBusiness
+ ? isAdmin && selectedDepartment?.SGid
+ ? selectedDepartment.SGid
+ : isAdmin
+ ? null
+ : session.currentSGid
+ : null;
+ const {
+ getLobby,
+ getSubgroup,
+ getTeams,
+ getTeamMembers,
+ getGroupMembers,
+ getCaches,
+ getUser,
+ deleteTeam,
+ resetPlayerProgress,
+ resetTeamProgress,
+ getSubgroups,
+ } = useGameHook();
-// State ----------------------
+ // State ----------------------
- const [loading, setLoading] = useState(true);
- const [teamsEnabled, setTeamsEnabled] = useState(false);
- const [activeTab, setActiveTab] = useState('game');
- const [teamRankings, setTeamRankings] = useState([]);
- const [playerRankings, setPlayerRankings] = useState([]);
- const [myTeamRanking, setMyTeamRanking] = useState([]);
- const [expandedTeam, setExpandedTeam] = useState(null);
- const [totalCacheCount, setTotalCacheCount] = useState(0);
- const [departmentList, setDepartmentList] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [teamsEnabled, setTeamsEnabled] = useState(false);
+ const [activeTab, setActiveTab] = useState("game");
+ const [teamRankings, setTeamRankings] = useState([]);
+ const [playerRankings, setPlayerRankings] = useState([]);
+ const [myTeamRanking, setMyTeamRanking] = useState([]);
+ const [expandedTeam, setExpandedTeam] = useState(null);
+ const [totalCacheCount, setTotalCacheCount] = useState(0);
+ const [departmentList, setDepartmentList] = useState([]);
-// Handlers -------------------
-
- const loadData = useCallback(async () => {
- if (!session.currentGid) { setLoading(false); return; }
-
- const group = await getLobby(session.currentGid);
-
- let isTeams = Boolean(group?.TeamsEnabled);
- if (effectiveSGid !== null && effectiveSGid !== undefined) {
- const subgroup = await getSubgroup(effectiveSGid);
- if (subgroup) isTeams = Boolean(subgroup.TeamsEnabled);
- }
- setTeamsEnabled(isTeams);
-
- if (isBusiness && isAdmin && !selectedDepartment) {
- const sgs = await getSubgroups(session.currentGid);
- setDepartmentList((sgs || []).filter((sg) => !sg.IsAdminGroup));
- setLoading(false);
- return;
- }
-
- const caches = await getCaches(session.currentGid, effectiveSGid ?? null);
- const total = (caches || []).length;
- setTotalCacheCount(total);
-
- if (isTeams) {
- // Build team rankings
- const teams = await getTeams(session.currentGid, effectiveSGid ?? null);
- const teamData = [];
-
- for (const team of (teams || [])) {
- const members = await getTeamMembers(team.Tid);
- const memberData = [];
- for (const m of (members || [])) {
- const user = await getUser(m.Uid);
- const count = (caches || []).filter((c) =>
- (c.Claims || []).some((cl) => cl.Uid === m.Uid)
- ).length;
- memberData.push({
- uid: m.Uid,
- name: user?.username || `Player ${m.Uid}`,
- caches: count,
- isLeader: Boolean(m.IsLeader),
- });
- }
- memberData.sort((a, b) => b.caches - a.caches);
- const totalCaches = (caches || []).filter((c) =>
- (c.Claims || []).some((cl) => cl.Tid === team.Tid)
- ).length;
- teamData.push({
- tid: team.Tid,
- name: team.TeamName || 'Unnamed Team',
- code: team.JoinCode,
- totalCaches,
- members: memberData,
- });
- }
-
- teamData.sort((a, b) => b.totalCaches - a.totalCaches);
- setTeamRankings(teamData);
-
- // Build current user's team ranking (for TEAM tab)
- if (session.currentTid) {
- const myTeam = teamData.find((t) => t.tid === session.currentTid);
- setMyTeamRanking(myTeam ? myTeam.members : []);
- }
- } else {
- // Build player rankings (no teams)
- const scopedMembers = await getGroupMembers(session.currentGid, effectiveSGid ?? null);
- const playerData = [];
- for (const m of (scopedMembers || []).filter((mem) => !mem.IsAcceptedAdmin)) {
- const user = await getUser(m.Uid);
- const count = (caches || []).filter((c) =>
- (c.Claims || []).some((cl) => cl.Uid === m.Uid)
- ).length;
- playerData.push({
- uid: m.Uid,
- name: user?.username || `Player ${m.Uid}`,
- caches: count,
- });
- }
- playerData.sort((a, b) => b.caches - a.caches);
- setPlayerRankings(playerData);
- }
-
- setLoading(false);
- }, [session.currentGid, session.currentTid, session.currentSGid, isBusiness, isAdmin, selectedDepartment, selectedDepartment?.SGid, effectiveSGid]);
-
- useEffect(() => { loadData(); }, [loadData]);
-
- // Admin handler: delete a team and reload
- const handleDeleteTeam = (team) => {
- Alert.alert('Delete Team', `Delete "${team.name}" and kick all its members?`, [
- {text: 'Cancel', style: 'cancel'},
- {
- text: 'Delete',
- style: 'destructive',
- onPress: async () => {
- await deleteTeam(team.tid);
- await loadData();
- },
- },
- ]);
- };
-
- // Admin handler: reset a single player's progress
- const handleResetPlayer = (player) => {
- Alert.alert('Reset Progress', `Reset all cache claims for ${player.name}?`, [
- {text: 'Cancel', style: 'cancel'},
- {
- text: 'Reset',
- style: 'destructive',
- onPress: async () => {
- await resetPlayerProgress(session.currentGid, player.uid, effectiveSGid);
- Alert.alert('Done', `Progress for ${player.name} has been reset.`);
- await loadData();
- },
- },
- ]);
- };
-
- const handleResetTeam = (team) => {
- Alert.alert('Reset Team Progress', `Reset all cache claims for team "${team.name}"?`, [
- {text: 'Cancel', style: 'cancel'},
- {
- text: 'Reset',
- style: 'destructive',
- onPress: async () => {
- await resetTeamProgress(session.currentGid, team.tid, effectiveSGid);
- Alert.alert('Done', `Progress for team "${team.name}" has been reset.`);
- await loadData();
- },
- },
- ]);
- };
-
-// View -----------------------
-
- if (loading) {
- return (
-
-
-
- );
- }
+ // Handlers -------------------
+ const loadData = useCallback(async () => {
if (!session.currentGid) {
- return (
-
- Join a game to view the leaderboard.
-
- );
+ setLoading(false);
+ return;
}
- if (session.isPendingAdmin && !session.isAcceptedAdmin) {
- return ;
+ const group = await getLobby(session.currentGid);
+
+ let isTeams = Boolean(group?.TeamsEnabled);
+ if (effectiveSGid !== null && effectiveSGid !== undefined) {
+ const subgroup = await getSubgroup(effectiveSGid);
+ if (subgroup) isTeams = Boolean(subgroup.TeamsEnabled);
}
+ setTeamsEnabled(isTeams);
if (isBusiness && isAdmin && !selectedDepartment) {
- return (
-
- Departments
-
- {departmentList.map((department) => (
- navigation.navigate('LeaderboardScreen', {department})}
- >
-
-
- {department.SubGroupName}
- Open
-
-
-
- ))}
- {departmentList.length === 0 && (
- No departments yet.
- )}
-
-
- );
+ const sgs = await getSubgroups(session.currentGid);
+ setDepartmentList((sgs || []).filter((sg) => !sg.IsAdminGroup));
+ setLoading(false);
+ return;
}
- // Helper: render an expandable team card (admin version with delete + reset)
- const renderAdminTeamCard = (team, index) => (
-
- setExpandedTeam(expandedTeam === team.tid ? null : team.tid)}>
-
- #{index + 1}
- {team.name} ({team.code})
- {team.totalCaches}/{totalCacheCount}
-
-
- {expandedTeam === team.tid && (
-
- {team.members.map((m) => (
- // Highlight the current user's row so they can identify themselves
-
-
- {m.name}{m.isLeader ? ' (Team Leader)' : ''}
-
- {m.caches}/{totalCacheCount}
- handleResetPlayer(m)}
- >
- Reset
-
-
- ))}
-
- handleResetTeam(team)}
- styleButton={styles.resetTeamButton}
- styleLabel={styles.deleteTeamLabel}
- />
- handleDeleteTeam(team)}
- styleButton={styles.deleteTeamButton}
- styleLabel={styles.deleteTeamDeleteLabel}
- />
-
-
- )}
-
- );
+ const caches = await getCaches(session.currentGid, effectiveSGid ?? null);
+ const total = (caches || []).length;
+ setTotalCacheCount(total);
- // Helper: render an expandable team card (player version)
- const renderTeamCard = (team, index) => (
- setExpandedTeam(expandedTeam === team.tid ? null : team.tid)}>
-
-
- #{index + 1}
- {team.name} ({team.code})
- {team.totalCaches}/{totalCacheCount}
-
- {expandedTeam === team.tid && team.members.map((m) => (
- // Highlight the current user's row so they can identify themselves
-
-
- {m.name}{m.isLeader ? ' (Team Leader)' : ''}
-
- {m.caches}/{totalCacheCount}
-
- ))}
-
-
- );
+ if (isTeams) {
+ // Build team rankings
+ const teams = await getTeams(session.currentGid, effectiveSGid ?? null);
+ const teamData = [];
- // Helper: render an admin player card (with reset button)
- const renderAdminPlayerCard = (player, index) => (
- // Highlight the current user's card so they can identify themselves
-
-
- #{index + 1}
- {player.name}
- {player.caches}/{totalCacheCount}
- handleResetPlayer(player)}
- >
- Reset
-
-
-
- );
-
- // Helper: render a simple player card
- const renderPlayerCard = (player, index) => (
- // Highlight the current user's card so they can identify themselves
-
-
- #{index + 1}
- {player.name}
- {player.caches}/{totalCacheCount}
-
-
- );
-
- // ---------- ADMIN LEADERBOARD ----------
- if (isAdmin) {
- // Teams enabled: show team list (no tabs for admin)
- if (teamsEnabled) {
- return (
-
- {selectedDepartment && (
- {selectedDepartment.SubGroupName}
- )}
-
- {teamRankings.map((team, index) => renderAdminTeamCard(team, index))}
- {teamRankings.length === 0 && (
- No teams yet.
- )}
-
-
- );
+ for (const team of teams || []) {
+ const members = await getTeamMembers(team.Tid);
+ const memberData = [];
+ for (const m of members || []) {
+ const user = await getUser(m.Uid);
+ const count = (caches || []).filter((c) =>
+ (c.Claims || []).some((cl) => cl.Uid === m.Uid),
+ ).length;
+ memberData.push({
+ uid: m.Uid,
+ name: user?.username || `Player ${m.Uid}`,
+ caches: count,
+ isLeader: Boolean(m.IsLeader),
+ });
}
+ memberData.sort((a, b) => b.caches - a.caches);
+ const totalCaches = (caches || []).filter((c) =>
+ (c.Claims || []).some((cl) => cl.Tid === team.Tid),
+ ).length;
+ teamData.push({
+ tid: team.Tid,
+ name: team.TeamName || "Unnamed Team",
+ code: team.JoinCode,
+ totalCaches,
+ members: memberData,
+ });
+ }
- // Teams disabled: show player list with reset buttons
- return (
-
- {selectedDepartment && (
- {selectedDepartment.SubGroupName}
- )}
-
- {playerRankings.map((player, index) => renderAdminPlayerCard(player, index))}
- {playerRankings.length === 0 && (
- No players yet.
- )}
-
-
- );
+ teamData.sort((a, b) => b.totalCaches - a.totalCaches);
+ setTeamRankings(teamData);
+
+ // Build current user's team ranking (for TEAM tab)
+ if (session.currentTid) {
+ const myTeam = teamData.find((t) => t.tid === session.currentTid);
+ setMyTeamRanking(myTeam ? myTeam.members : []);
+ }
+ } else {
+ // Build player rankings (no teams)
+ const scopedMembers = await getGroupMembers(
+ session.currentGid,
+ effectiveSGid ?? null,
+ );
+ const playerData = [];
+ for (const m of (scopedMembers || []).filter(
+ (mem) => !mem.IsAcceptedAdmin,
+ )) {
+ const user = await getUser(m.Uid);
+ const count = (caches || []).filter((c) =>
+ (c.Claims || []).some((cl) => cl.Uid === m.Uid),
+ ).length;
+ playerData.push({
+ uid: m.Uid,
+ name: user?.username || `Player ${m.Uid}`,
+ caches: count,
+ });
+ }
+ playerData.sort((a, b) => b.caches - a.caches);
+ setPlayerRankings(playerData);
}
- // ---------- PLAYER LEADERBOARD ----------
+ setLoading(false);
+ }, [
+ session.currentGid,
+ session.currentTid,
+ session.currentSGid,
+ isBusiness,
+ isAdmin,
+ selectedDepartment,
+ selectedDepartment?.SGid,
+ effectiveSGid,
+ ]);
- // Teams enabled — tabbed view (GAME / TEAM)
- if (teamsEnabled) {
- return (
-
-
- setActiveTab('game')}
- >
- GAME
-
- setActiveTab('team')}
- >
- TEAM
-
-
+ useEffect(() => {
+ loadData();
+ }, [loadData]);
- {activeTab === 'game' ? (
-
- {teamRankings.map((team, index) => renderTeamCard(team, index))}
- {teamRankings.length === 0 && (
- No teams yet.
- )}
-
- ) : (
-
- {myTeamRanking.map((m, index) => (
- // Highlight the current user's card so they can identify themselves
-
-
- #{index + 1}
-
- {m.name}{m.isLeader ? ' (Team Leader)' : ''}
-
- {m.caches}/{totalCacheCount}
-
-
- ))}
- {myTeamRanking.length === 0 && (
- Join a team to see your team leaderboard.
- )}
-
- )}
-
- );
- }
-
- // No teams — simple player ranking (GAME tab only, no TEAM tab)
- return (
-
-
- {playerRankings.map((player, index) => renderPlayerCard(player, index))}
- {playerRankings.length === 0 && (
- No players yet.
- )}
-
-
+ // Admin handler: delete a team and reload
+ const handleDeleteTeam = (team) => {
+ Alert.alert(
+ "Delete Team",
+ `Delete "${team.name}" and kick all its members?`,
+ [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Delete",
+ style: "destructive",
+ onPress: async () => {
+ await deleteTeam(team.tid);
+ await loadData();
+ },
+ },
+ ],
);
+ };
+
+ // Admin handler: reset a single player's progress
+ const handleResetPlayer = (player) => {
+ Alert.alert(
+ "Reset Progress",
+ `Reset all cache claims for ${player.name}?`,
+ [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Reset",
+ style: "destructive",
+ onPress: async () => {
+ await resetPlayerProgress(
+ session.currentGid,
+ player.uid,
+ effectiveSGid,
+ );
+ Alert.alert("Done", `Progress for ${player.name} has been reset.`);
+ await loadData();
+ },
+ },
+ ],
+ );
+ };
+
+ const handleResetTeam = (team) => {
+ Alert.alert(
+ "Reset Team Progress",
+ `Reset all cache claims for team "${team.name}"?`,
+ [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Reset",
+ style: "destructive",
+ onPress: async () => {
+ await resetTeamProgress(
+ session.currentGid,
+ team.tid,
+ effectiveSGid,
+ );
+ Alert.alert(
+ "Done",
+ `Progress for team "${team.name}" has been reset.`,
+ );
+ await loadData();
+ },
+ },
+ ],
+ );
+ };
+
+ // View -----------------------
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (!session.currentGid) {
+ return (
+
+ Join a game to view the leaderboard.
+
+ );
+ }
+
+ if (session.isPendingAdmin && !session.isAcceptedAdmin) {
+ return ;
+ }
+
+ if (isBusiness && isAdmin && !selectedDepartment) {
+ return (
+
+ Departments
+
+ {departmentList.map((department) => (
+
+ navigation.navigate("LeaderboardScreen", { department })
+ }
+ >
+
+
+ {department.SubGroupName}
+ Open
+
+
+
+ ))}
+ {departmentList.length === 0 && (
+ No departments yet.
+ )}
+
+
+ );
+ }
+
+ // Helper: render an expandable team card (admin version with delete + reset)
+ const renderAdminTeamCard = (team, index) => (
+
+
+ setExpandedTeam(expandedTeam === team.tid ? null : team.tid)
+ }
+ >
+
+ #{index + 1}
+
+ {team.name} ({team.code})
+
+
+ {team.totalCaches}/{totalCacheCount}
+
+
+
+ {expandedTeam === team.tid && (
+
+ {team.members.map((m) => (
+ // Highlight the current user's row so they can identify themselves
+
+
+ {m.name}
+ {m.isLeader ? " (Team Leader)" : ""}
+
+
+ {m.caches}/{totalCacheCount}
+
+ handleResetPlayer(m)}
+ >
+ Reset
+
+
+ ))}
+
+ handleResetTeam(team)}
+ styleButton={styles.resetTeamButton}
+ styleLabel={styles.deleteTeamLabel}
+ />
+ handleDeleteTeam(team)}
+ styleButton={styles.deleteTeamButton}
+ styleLabel={styles.deleteTeamDeleteLabel}
+ />
+
+
+ )}
+
+ );
+
+ // Helper: render an expandable team card (player version)
+ const renderTeamCard = (team, index) => (
+
+ setExpandedTeam(expandedTeam === team.tid ? null : team.tid)
+ }
+ >
+
+
+ #{index + 1}
+
+ {team.name} ({team.code})
+
+
+ {team.totalCaches}/{totalCacheCount}
+
+
+ {expandedTeam === team.tid &&
+ team.members.map((m) => (
+ // Highlight the current user's row so they can identify themselves
+
+
+ {m.name}
+ {m.isLeader ? " (Team Leader)" : ""}
+
+
+ {m.caches}/{totalCacheCount}
+
+
+ ))}
+
+
+ );
+
+ // Helper: render an admin player card (with reset button)
+ const renderAdminPlayerCard = (player, index) => (
+ // Highlight the current user's card so they can identify themselves
+
+
+ #{index + 1}
+ {player.name}
+
+ {player.caches}/{totalCacheCount}
+
+ handleResetPlayer(player)}
+ >
+ Reset
+
+
+
+ );
+
+ // Helper: render a simple player card
+ const renderPlayerCard = (player, index) => (
+ // Highlight the current user's card so they can identify themselves
+
+
+ #{index + 1}
+ {player.name}
+
+ {player.caches}/{totalCacheCount}
+
+
+
+ );
+
+ // ---------- ADMIN LEADERBOARD ----------
+ if (isAdmin) {
+ // Teams enabled: show team list (no tabs for admin)
+ if (teamsEnabled) {
+ return (
+
+ {selectedDepartment && (
+
+ {selectedDepartment.SubGroupName}
+
+ )}
+
+ {teamRankings.map((team, index) =>
+ renderAdminTeamCard(team, index),
+ )}
+ {teamRankings.length === 0 && (
+ No teams yet.
+ )}
+
+
+ );
+ }
+
+ // Teams disabled: show player list with reset buttons
+ return (
+
+ {selectedDepartment && (
+
+ {selectedDepartment.SubGroupName}
+
+ )}
+
+ {playerRankings.map((player, index) =>
+ renderAdminPlayerCard(player, index),
+ )}
+ {playerRankings.length === 0 && (
+ No players yet.
+ )}
+
+
+ );
+ }
+
+ // ---------- PLAYER LEADERBOARD ----------
+
+ // Teams enabled — tabbed view (GAME / TEAM)
+ if (teamsEnabled) {
+ return (
+
+
+ setActiveTab("game")}
+ >
+
+ GAME
+
+
+ setActiveTab("team")}
+ >
+
+ TEAM
+
+
+
+
+ {activeTab === "game" ? (
+
+ {teamRankings.map((team, index) => renderTeamCard(team, index))}
+ {teamRankings.length === 0 && (
+ No teams yet.
+ )}
+
+ ) : (
+
+ {myTeamRanking.map((m, index) => (
+ // Highlight the current user's card so they can identify themselves
+
+
+ #{index + 1}
+
+ {m.name}
+ {m.isLeader ? " (Team Leader)" : ""}
+
+
+ {m.caches}/{totalCacheCount}
+
+
+
+ ))}
+ {myTeamRanking.length === 0 && (
+
+ Join a team to see your team leaderboard.
+
+ )}
+
+ )}
+
+ );
+ }
+
+ // No teams — simple player ranking (GAME tab only, no TEAM tab)
+ return (
+
+
+ {playerRankings.map((player, index) => renderPlayerCard(player, index))}
+ {playerRankings.length === 0 && (
+ No players yet.
+ )}
+
+
+ );
};
const styles = StyleSheet.create({
- center: {justifyContent: 'center', alignItems: 'center'},
- container: {padding: 0},
- body: {color: '#bac2de', fontSize: 15},
- departmentTitle: {fontSize: 20, fontWeight: '700', color: '#cdd6f4', paddingHorizontal: 15, paddingTop: 15, marginBottom: 6},
- openLabel: {fontSize: 13, fontWeight: '700', color: '#bd93f9'},
- tabRow: {
- flexDirection: 'row',
- borderBottomWidth: 1,
- borderBottomColor: '#45475a',
- },
- tabButton: {
- flex: 1,
- paddingVertical: 12,
- alignItems: 'center',
- },
- tabButtonActive: {
- borderBottomWidth: 2,
- borderBottomColor: '#bd93f9',
- },
- tabLabel: {fontSize: 15, fontWeight: '600', color: '#6c7086'},
- tabLabelActive: {color: '#bd93f9'},
- listSection: {flex: 1, paddingHorizontal: 15, paddingTop: 10},
- rankRow: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 10,
- },
- rank: {fontSize: 16, fontWeight: '700', color: '#bac2de', width: 32},
- rankName: {flex: 1, fontSize: 15, fontWeight: '600', color: '#cdd6f4'},
- score: {fontSize: 14, fontWeight: '600', color: '#a6e3a1'},
- subRow: {
- flexDirection: 'row',
- alignItems: 'center',
- paddingLeft: 42,
- paddingVertical: 6,
- borderTopWidth: 1,
- borderTopColor: '#45475a',
- },
- subName: {fontSize: 13, color: '#bac2de'},
- subScore: {fontSize: 13, fontWeight: '600', color: '#a6e3a1', marginRight: 8},
- emptyText: {color: '#6c7086', textAlign: 'center', marginTop: 30, fontSize: 14},
- deleteTeamWrap: {marginTop: 8, paddingLeft: 42, flexDirection: 'row', gap: 8},
- resetTeamButton: {backgroundColor: '#f4a460', borderColor: '#f4a460', minHeight: 36, flex: 1},
- deleteTeamButton: {backgroundColor: '#D92800', borderColor: '#D92800', minHeight: 36, flex: 1},
- deleteTeamLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 13},
- deleteTeamDeleteLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13},
- resetMiniButton: {
- backgroundColor: '#86efac',
- borderRadius: 6,
- paddingHorizontal: 10,
- paddingVertical: 4,
- marginLeft: 6,
- },
- resetMiniLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 12},
- selfCard: {backgroundColor: 'rgba(166, 227, 161, 0.15)', borderColor: '#a6e3a1'},
- selfSubRow: {backgroundColor: 'rgba(166, 227, 161, 0.15)'},
+ center: { justifyContent: "center", alignItems: "center" },
+ container: { padding: 0 },
+ body: { color: "#bac2de", fontSize: 15 },
+ departmentTitle: {
+ fontSize: 20,
+ fontWeight: "700",
+ color: "#cdd6f4",
+ paddingHorizontal: 15,
+ paddingTop: 15,
+ marginBottom: 6,
+ },
+ openLabel: { fontSize: 13, fontWeight: "700", color: "#bd93f9" },
+ tabRow: {
+ flexDirection: "row",
+ borderBottomWidth: 1,
+ borderBottomColor: "#45475a",
+ },
+ tabButton: {
+ flex: 1,
+ paddingVertical: 12,
+ alignItems: "center",
+ },
+ tabButtonActive: {
+ borderBottomWidth: 2,
+ borderBottomColor: "#bd93f9",
+ },
+ tabLabel: { fontSize: 15, fontWeight: "600", color: "#6c7086" },
+ tabLabelActive: { color: "#bd93f9" },
+ listSection: { flex: 1, paddingHorizontal: 15, paddingTop: 10 },
+ rankRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 10,
+ },
+ rank: { fontSize: 16, fontWeight: "700", color: "#bac2de", width: 32 },
+ rankName: { flex: 1, fontSize: 15, fontWeight: "600", color: "#cdd6f4" },
+ score: { fontSize: 14, fontWeight: "600", color: "#a6e3a1" },
+ subRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ paddingLeft: 42,
+ paddingVertical: 6,
+ borderTopWidth: 1,
+ borderTopColor: "#45475a",
+ },
+ subName: { fontSize: 13, color: "#bac2de" },
+ subScore: {
+ fontSize: 13,
+ fontWeight: "600",
+ color: "#a6e3a1",
+ marginRight: 8,
+ },
+ emptyText: {
+ color: "#6c7086",
+ textAlign: "center",
+ marginTop: 30,
+ fontSize: 14,
+ },
+ deleteTeamWrap: {
+ marginTop: 8,
+ paddingLeft: 42,
+ flexDirection: "row",
+ gap: 8,
+ },
+ resetTeamButton: {
+ backgroundColor: "#f4a460",
+ borderColor: "#f4a460",
+ minHeight: 36,
+ flex: 1,
+ },
+ deleteTeamButton: {
+ backgroundColor: "#D92800",
+ borderColor: "#D92800",
+ minHeight: 36,
+ flex: 1,
+ },
+ deleteTeamLabel: { color: "#1e1e2e", fontWeight: "600", fontSize: 13 },
+ deleteTeamDeleteLabel: { color: "#ffffff", fontWeight: "600", fontSize: 13 },
+ resetMiniButton: {
+ backgroundColor: "#86efac",
+ borderRadius: 6,
+ paddingHorizontal: 10,
+ paddingVertical: 4,
+ marginLeft: 6,
+ },
+ resetMiniLabel: { color: "#1e1e2e", fontWeight: "600", fontSize: 12 },
+ selfCard: {
+ backgroundColor: "rgba(166, 227, 161, 0.15)",
+ borderColor: "#a6e3a1",
+ },
+ selfSubRow: { backgroundColor: "rgba(166, 227, 161, 0.15)" },
});
export default LeaderboardScreen;
diff --git a/src/components/screens/LoginScreen.js b/src/components/screens/LoginScreen.js
index 9c001bc..b7d4ee6 100644
--- a/src/components/screens/LoginScreen.js
+++ b/src/components/screens/LoginScreen.js
@@ -1,11 +1,12 @@
-import React, {useState} from 'react';
-import {Pressable, StyleSheet, Text, TextInput, View} from 'react-native';
-import useAuthHook from '../../hooks/useAuthHook';
-import Screen from '../layout/Screen';
-import {Button, ButtonTray} from '../UI/Button';
+import { useState } from "react";
+import { Pressable, StyleSheet, Text, TextInput, View } from "react-native";
+import useAuthHook from "../../hooks/useAuthHook";
+import Screen from "../layout/Screen";
+import { Button, ButtonTray } from "../UI/Button";
const LoginScreen = ({ navigation }) => {
- const { email, password, error, isLoading, setEmail, setPassword, login } = useAuthHook();
+ const { email, password, error, isLoading, setEmail, setPassword, login } =
+ useAuthHook();
const [submitting, setSubmitting] = useState(false);
const handleLogin = async () => {
@@ -17,10 +18,10 @@ const LoginScreen = ({ navigation }) => {
}
const rootNav = navigation.getParent() || navigation;
if (user.IsAcceptedAdmin) {
- rootNav.reset({ index: 0, routes: [{ name: 'Admin' }] });
+ rootNav.reset({ index: 0, routes: [{ name: "Admin" }] });
return;
}
- rootNav.reset({ index: 0, routes: [{ name: 'Game' }] });
+ rootNav.reset({ index: 0, routes: [{ name: "Game" }] });
};
return (
@@ -52,14 +53,14 @@ const LoginScreen = ({ navigation }) => {
- navigation.navigate('RegisterScreen')}>
+ navigation.navigate("RegisterScreen")}>
Create new account
@@ -69,45 +70,45 @@ const LoginScreen = ({ navigation }) => {
const styles = StyleSheet.create({
container: {
- justifyContent: 'center',
+ justifyContent: "center",
},
content: {
gap: 15,
},
title: {
fontSize: 24,
- fontWeight: 'bold',
- textAlign: 'center',
+ fontWeight: "bold",
+ textAlign: "center",
marginBottom: 10,
- color: '#cdd6f4',
+ color: "#cdd6f4",
},
input: {
borderWidth: 1,
- borderColor: '#45475a',
+ borderColor: "#45475a",
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 10,
fontSize: 16,
- color: '#cdd6f4',
- backgroundColor: '#313244',
+ color: "#cdd6f4",
+ backgroundColor: "#313244",
},
errorText: {
- color: '#D92800',
- textAlign: 'center',
+ color: "#D92800",
+ textAlign: "center",
marginTop: 5,
},
submitButton: {
- backgroundColor: '#bd93f9',
- borderColor: '#bd93f9',
+ backgroundColor: "#bd93f9",
+ borderColor: "#bd93f9",
},
submitText: {
- color: '#1e1e2e',
- fontWeight: '600',
+ color: "#1e1e2e",
+ fontWeight: "600",
},
linkText: {
- textAlign: 'center',
- color: '#bd93f9',
- fontWeight: '600',
+ textAlign: "center",
+ color: "#bd93f9",
+ fontWeight: "600",
paddingVertical: 10,
},
});
diff --git a/src/components/screens/PlayersScreen.js b/src/components/screens/PlayersScreen.js
index af0b5f5..466ad06 100644
--- a/src/components/screens/PlayersScreen.js
+++ b/src/components/screens/PlayersScreen.js
@@ -1,411 +1,525 @@
-import React, {useCallback, useEffect, useState} from 'react';
-import {ActivityIndicator, Alert, Pressable, ScrollView, StyleSheet, Text, View} from 'react-native';
-import Screen from '../layout/Screen';
-import Card from '../UI/Card';
-import {Button} from '../UI/Button';
-import PendingApprovalView from '../UI/PendingApprovalView';
-import useGameHook from '../../hooks/useGameHook';
-import {getSession} from '../../hooks/SessionStore';
+import { useCallback, useEffect, useState } from "react";
+import {
+ ActivityIndicator,
+ Alert,
+ Pressable,
+ ScrollView,
+ StyleSheet,
+ Text,
+ View,
+} from "react-native";
+import Screen from "../layout/Screen";
+import Card from "../UI/Card";
+import { Button } from "../UI/Button";
+import PendingApprovalView from "../UI/PendingApprovalView";
+import useGameHook from "../../hooks/useGameHook";
+import { getSession } from "../../hooks/SessionStore";
-const PlayersScreen = ({navigation, route}) => {
-// Initialisation ------------
+const PlayersScreen = ({ navigation, route }) => {
+ // Initialisation ------------
- const session = getSession();
- const {getTeams, getTeamMembers, getGroupMembers, removeMember, resetPlayerProgress, getUser, getAdminWaitlist, approveAdmin, rejectAdmin, getSubgroups} = useGameHook();
- const isBusiness = Boolean(session.isBusiness);
- const isAdmin = Boolean(session.isAcceptedAdmin);
- const selectedDepartment = route?.params?.department || null;
- const effectiveSGid = isBusiness
- ? ((isAdmin && selectedDepartment?.SGid) ? selectedDepartment.SGid : (isAdmin ? null : session.currentSGid))
- : null;
+ const session = getSession();
+ const {
+ getTeams,
+ getTeamMembers,
+ getGroupMembers,
+ removeMember,
+ resetPlayerProgress,
+ getUser,
+ getAdminWaitlist,
+ approveAdmin,
+ rejectAdmin,
+ getSubgroups,
+ } = useGameHook();
+ const isBusiness = Boolean(session.isBusiness);
+ const isAdmin = Boolean(session.isAcceptedAdmin);
+ const selectedDepartment = route?.params?.department || null;
+ const effectiveSGid = isBusiness
+ ? isAdmin && selectedDepartment?.SGid
+ ? selectedDepartment.SGid
+ : isAdmin
+ ? null
+ : session.currentSGid
+ : null;
-// State ----------------------
+ // State ----------------------
- const [loading, setLoading] = useState(true);
- const [players, setPlayers] = useState([]);
- const [departmentList, setDepartmentList] = useState([]);
- const [adminTeam, setAdminTeam] = useState([]);
- const [isCurrentUserAdminLead, setIsCurrentUserAdminLead] = useState(false);
- const [adminTeamExpanded, setAdminTeamExpanded] = useState(false);
+ const [loading, setLoading] = useState(true);
+ const [players, setPlayers] = useState([]);
+ const [departmentList, setDepartmentList] = useState([]);
+ const [adminTeam, setAdminTeam] = useState([]);
+ const [isCurrentUserAdminLead, setIsCurrentUserAdminLead] = useState(false);
+ const [adminTeamExpanded, setAdminTeamExpanded] = useState(false);
-// Handlers -------------------
-
- const loadData = useCallback(async () => {
- if (!session.currentGid) { setLoading(false); return; }
-
- if (isBusiness && isAdmin && !selectedDepartment) {
- const sgs = await getSubgroups(session.currentGid);
- setDepartmentList((sgs || []).filter((sg) => !sg.IsAdminGroup));
-
- const allOrgMembers = await getGroupMembers(session.currentGid, null);
- const seenAdminUids = new Set();
- const acceptedAdmins = (allOrgMembers || [])
- .filter((m) => m.IsAcceptedAdmin)
- .sort((a, b) => Number(a.id) - Number(b.id))
- .filter((m) => {
- if (seenAdminUids.has(m.Uid)) return false;
- seenAdminUids.add(m.Uid);
- return true;
- });
-
- const adminRows = [];
- for (const member of acceptedAdmins) {
- const user = await getUser(member.Uid);
- if (!user) continue;
- adminRows.push({
- Uid: member.Uid,
- username: user.username || `User ${member.Uid}`,
- membershipId: member.id,
- });
- }
- setAdminTeam(adminRows);
- setIsCurrentUserAdminLead(adminRows.length > 0 && adminRows[0].Uid === session.currentUid);
- setPlayers([]);
- setLoading(false);
- return;
- }
-
- setAdminTeam([]);
- setIsCurrentUserAdminLead(false);
-
- // Load subgroup names for department display (business accounts)
- let subgroupNameMap = {};
- if (isBusiness) {
- const sgs = await getSubgroups(session.currentGid);
- for (const sg of (sgs || [])) {
- if (!sg.IsAdminGroup) subgroupNameMap[sg.SGid] = sg.SubGroupName;
- }
- }
-
- // Get ALL members (including admins)
- const allMembers = await getGroupMembers(session.currentGid, effectiveSGid ?? null);
-
- // Get admin waitlist entries
- const waitlist = effectiveSGid ? [] : 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, effectiveSGid ?? null);
- const teamInfoByUid = {};
- for (const team of (allTeams || [])) {
- const tms = await getTeamMembers(team.Tid);
- for (const tm of (tms || [])) {
- teamInfoByUid[tm.Uid] = {
- teamCode: team.JoinCode,
- teamName: team.TeamName,
- isLeader: Boolean(tm.IsLeader),
- };
- }
- }
-
- // Build player list — include admins with tags
- const playerList = [];
- 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;
- 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,
- departmentName: subgroupNameMap[m.SGid] || null,
- });
- }
- }
-
- // 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;
- const user = await getUser(w.Uid);
- if (user) {
- playerList.push({
- ...user,
- membershipId: null,
- team: null,
- adminTag: '[Admin Awaiting Response]',
- waitlistId: w.id,
- departmentName: null,
- });
- }
- }
-
- setPlayers(playerList);
- setLoading(false);
- }, [session.currentGid, session.currentUid, session.currentSGid, isBusiness, isAdmin, selectedDepartment, selectedDepartment?.SGid, effectiveSGid]);
-
- useEffect(() => { loadData(); }, [loadData]);
-
- const handleRemovePlayer = async (membershipId) => {
- await removeMember(membershipId);
- await loadData();
- };
-
- const handleResetPlayerProgress = (player) => {
- Alert.alert(
- 'Reset Progress',
- `Reset all cache claims for ${player.username}?`,
- [
- {text: 'Cancel', style: 'cancel'},
- {
- text: 'Reset',
- style: 'destructive',
- onPress: async () => {
- await resetPlayerProgress(session.currentGid, player.Uid, effectiveSGid);
- Alert.alert('Done', `Progress for ${player.username} has been reset.`);
- },
- },
- ],
- );
- };
-
- // 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();
- };
-
- const handleKickAdmin = (adminMember) => {
- if (!isCurrentUserAdminLead || adminMember.Uid === session.currentUid) return;
- Alert.alert(
- 'Kick Admin',
- `Remove ${adminMember.username} from the admin team?`,
- [
- {text: 'Cancel', style: 'cancel'},
- {
- text: 'Kick',
- style: 'destructive',
- onPress: async () => {
- await removeMember(adminMember.membershipId);
- await loadData();
- },
- },
- ],
- );
- };
-
-// View -----------------------
-
- if (loading) {
- return (
-
-
-
- );
- }
+ // Handlers -------------------
+ const loadData = useCallback(async () => {
if (!session.currentGid) {
- return (
-
- Create or join a game first.
-
- );
- }
-
- if (session.isPendingAdmin && !session.isAcceptedAdmin) {
- return ;
+ setLoading(false);
+ return;
}
if (isBusiness && isAdmin && !selectedDepartment) {
- return (
-
- Departments
-
-
- setAdminTeamExpanded((prev) => !prev)}>
-
- Admin Team
- {adminTeamExpanded ? 'Close' : 'Open'}
-
-
- {adminTeamExpanded && (
-
- {adminTeam.map((adminMember, index) => (
-
-
- {adminMember.username}{index === 0 ? ' (Admin Lead)' : ''}
-
- {isCurrentUserAdminLead && adminMember.Uid !== session.currentUid && (
- handleKickAdmin(adminMember)}
- styleButton={styles.removeButton}
- styleLabel={styles.kickLabel}
- />
- )}
-
- ))}
- {adminTeam.length === 0 && (
- No approved admins yet.
- )}
-
- )}
-
- {departmentList.map((department) => (
- navigation.navigate('PlayersScreen', {department})}
- >
-
-
- {department.SubGroupName}
- Open
-
-
-
- ))}
- {departmentList.length === 0 && (
- No departments yet.
- )}
-
-
- );
+ const sgs = await getSubgroups(session.currentGid);
+ setDepartmentList((sgs || []).filter((sg) => !sg.IsAdminGroup));
+
+ const allOrgMembers = await getGroupMembers(session.currentGid, null);
+ const seenAdminUids = new Set();
+ const acceptedAdmins = (allOrgMembers || [])
+ .filter((m) => m.IsAcceptedAdmin)
+ .sort((a, b) => Number(a.id) - Number(b.id))
+ .filter((m) => {
+ if (seenAdminUids.has(m.Uid)) return false;
+ seenAdminUids.add(m.Uid);
+ return true;
+ });
+
+ const adminRows = [];
+ for (const member of acceptedAdmins) {
+ const user = await getUser(member.Uid);
+ if (!user) continue;
+ adminRows.push({
+ Uid: member.Uid,
+ username: user.username || `User ${member.Uid}`,
+ membershipId: member.id,
+ });
+ }
+ setAdminTeam(adminRows);
+ setIsCurrentUserAdminLead(
+ adminRows.length > 0 && adminRows[0].Uid === session.currentUid,
+ );
+ setPlayers([]);
+ setLoading(false);
+ return;
}
- return (
-
- Players
-
- {players.map((player) => (
- // Waitlisted admins get a light red tint so they stand out from regular players
-
-
-
- {player.username}
- {player.adminTag && (
-
- {' '}{player.adminTag}
-
- )}
- {/* Show department name in brackets for non-admin players (org accounts) */}
- {!player.adminTag && player.departmentName && (
-
- {' '}({player.departmentName})
-
- )}
-
-
- {player.team?.isLeader && (
- (Team Leader)
- )}
- {player.team && (
- {player.team.teamCode}
- )}
-
-
-
- {/* Waitlisted admins get Approve / Deny instead of the normal player actions */}
- {player.waitlistId ? (
- <>
- handleApproveAdmin(player)}
- styleButton={styles.approveButton}
- styleLabel={styles.actionLabel}
- />
- handleDenyAdmin(player)}
- styleButton={styles.denyButton}
- styleLabel={styles.actionLabel}
- />
- >
- ) : (
- <>
- {!player.adminTag && (
- handleResetPlayerProgress(player)}
- styleButton={styles.resetButton}
- styleLabel={styles.actionLabel}
- />
- )}
- {player.membershipId && (
- handleRemovePlayer(player.membershipId)}
- styleButton={styles.removeButton}
- styleLabel={styles.kickLabel}
- />
- )}
- >
- )}
-
-
- ))}
- {players.length === 0 && (
- No players have joined yet.
- )}
-
-
+ setAdminTeam([]);
+ setIsCurrentUserAdminLead(false);
+
+ // Load subgroup names for department display (business accounts)
+ let subgroupNameMap = {};
+ if (isBusiness) {
+ const sgs = await getSubgroups(session.currentGid);
+ for (const sg of sgs || []) {
+ if (!sg.IsAdminGroup) subgroupNameMap[sg.SGid] = sg.SubGroupName;
+ }
+ }
+
+ // Get ALL members (including admins)
+ const allMembers = await getGroupMembers(
+ session.currentGid,
+ effectiveSGid ?? null,
);
+
+ // Get admin waitlist entries
+ const waitlist = effectiveSGid
+ ? []
+ : 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, effectiveSGid ?? null);
+ const teamInfoByUid = {};
+ for (const team of allTeams || []) {
+ const tms = await getTeamMembers(team.Tid);
+ for (const tm of tms || []) {
+ teamInfoByUid[tm.Uid] = {
+ teamCode: team.JoinCode,
+ teamName: team.TeamName,
+ isLeader: Boolean(tm.IsLeader),
+ };
+ }
+ }
+
+ // Build player list — include admins with tags
+ const playerList = [];
+ 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;
+ 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,
+ departmentName: subgroupNameMap[m.SGid] || null,
+ });
+ }
+ }
+
+ // 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;
+ const user = await getUser(w.Uid);
+ if (user) {
+ playerList.push({
+ ...user,
+ membershipId: null,
+ team: null,
+ adminTag: "[Admin Awaiting Response]",
+ waitlistId: w.id,
+ departmentName: null,
+ });
+ }
+ }
+
+ setPlayers(playerList);
+ setLoading(false);
+ }, [
+ session.currentGid,
+ session.currentUid,
+ session.currentSGid,
+ isBusiness,
+ isAdmin,
+ selectedDepartment,
+ selectedDepartment?.SGid,
+ effectiveSGid,
+ ]);
+
+ useEffect(() => {
+ loadData();
+ }, [loadData]);
+
+ const handleRemovePlayer = async (membershipId) => {
+ await removeMember(membershipId);
+ await loadData();
+ };
+
+ const handleResetPlayerProgress = (player) => {
+ Alert.alert(
+ "Reset Progress",
+ `Reset all cache claims for ${player.username}?`,
+ [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Reset",
+ style: "destructive",
+ onPress: async () => {
+ await resetPlayerProgress(
+ session.currentGid,
+ player.Uid,
+ effectiveSGid,
+ );
+ Alert.alert(
+ "Done",
+ `Progress for ${player.username} has been reset.`,
+ );
+ },
+ },
+ ],
+ );
+ };
+
+ // 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();
+ };
+
+ const handleKickAdmin = (adminMember) => {
+ if (!isCurrentUserAdminLead || adminMember.Uid === session.currentUid)
+ return;
+ Alert.alert(
+ "Kick Admin",
+ `Remove ${adminMember.username} from the admin team?`,
+ [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Kick",
+ style: "destructive",
+ onPress: async () => {
+ await removeMember(adminMember.membershipId);
+ await loadData();
+ },
+ },
+ ],
+ );
+ };
+
+ // View -----------------------
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (!session.currentGid) {
+ return (
+
+ Create or join a game first.
+
+ );
+ }
+
+ if (session.isPendingAdmin && !session.isAcceptedAdmin) {
+ return ;
+ }
+
+ if (isBusiness && isAdmin && !selectedDepartment) {
+ return (
+
+ Departments
+
+
+ setAdminTeamExpanded((prev) => !prev)}>
+
+ Admin Team
+
+ {adminTeamExpanded ? "Close" : "Open"}
+
+
+
+ {adminTeamExpanded && (
+
+ {adminTeam.map((adminMember, index) => (
+
+
+ {adminMember.username}
+ {index === 0 ? " (Admin Lead)" : ""}
+
+ {isCurrentUserAdminLead &&
+ adminMember.Uid !== session.currentUid && (
+ handleKickAdmin(adminMember)}
+ styleButton={styles.removeButton}
+ styleLabel={styles.kickLabel}
+ />
+ )}
+
+ ))}
+ {adminTeam.length === 0 && (
+ No approved admins yet.
+ )}
+
+ )}
+
+ {departmentList.map((department) => (
+
+ navigation.navigate("PlayersScreen", { department })
+ }
+ >
+
+
+
+ {department.SubGroupName}
+
+ Open
+
+
+
+ ))}
+ {departmentList.length === 0 && (
+ No departments yet.
+ )}
+
+
+ );
+ }
+
+ return (
+
+ Players
+
+ {players.map((player) => (
+ // Waitlisted admins get a light red tint so they stand out from regular players
+
+
+
+ {player.username}
+ {player.adminTag && (
+
+ {" "}
+ {player.adminTag}
+
+ )}
+ {/* Show department name in brackets for non-admin players (org accounts) */}
+ {!player.adminTag && player.departmentName && (
+
+ {" "}
+ ({player.departmentName})
+
+ )}
+
+
+ {player.team?.isLeader && (
+ (Team Leader)
+ )}
+ {player.team && (
+
+ {player.team.teamCode}
+
+ )}
+
+
+
+ {/* Waitlisted admins get Approve / Deny instead of the normal player actions */}
+ {player.waitlistId ? (
+ <>
+ handleApproveAdmin(player)}
+ styleButton={styles.approveButton}
+ styleLabel={styles.actionLabel}
+ />
+ handleDenyAdmin(player)}
+ styleButton={styles.denyButton}
+ styleLabel={styles.actionLabel}
+ />
+ >
+ ) : (
+ <>
+ {!player.adminTag && (
+ handleResetPlayerProgress(player)}
+ styleButton={styles.resetButton}
+ styleLabel={styles.actionLabel}
+ />
+ )}
+ {player.membershipId && (
+ handleRemovePlayer(player.membershipId)}
+ styleButton={styles.removeButton}
+ styleLabel={styles.kickLabel}
+ />
+ )}
+ >
+ )}
+
+
+ ))}
+ {players.length === 0 && (
+ No players have joined yet.
+ )}
+
+
+ );
};
const styles = StyleSheet.create({
- center: {justifyContent: 'center', alignItems: 'center', flex: 1},
- container: {padding: 0},
- body: {color: '#bac2de', fontSize: 15},
- sectionTitle: {
- fontSize: 20,
- fontWeight: '700',
- color: '#cdd6f4',
- marginBottom: 12,
- paddingHorizontal: 15,
- paddingTop: 15,
- },
- openLabel: {fontSize: 13, fontWeight: '700', color: '#bd93f9'},
- listSection: {flex: 1, paddingHorizontal: 15},
- emptyText: {color: '#6c7086', textAlign: 'center', marginTop: 30, fontSize: 14},
- playerRow: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 8,
- marginBottom: 8,
- },
- playerName: {fontSize: 15, fontWeight: '600', color: '#cdd6f4', flex: 1},
- playerMeta: {flexDirection: 'row', alignItems: 'center', gap: 6},
- leaderBadge: {fontSize: 11, color: '#bd93f9', fontWeight: '700'},
- teamCodeBadge: {fontSize: 12, fontWeight: '700', color: '#a6e3a1', letterSpacing: 1},
- adminBadge: {fontSize: 12, fontWeight: '700', color: '#D92800'},
- waitlistBadge: {fontSize: 11, fontWeight: '600', color: '#f4a460'},
- actionRow: {flexDirection: 'row', gap: 8},
- resetButton: {backgroundColor: '#86efac', borderColor: '#86efac', minHeight: 36, flex: 1, paddingHorizontal: 10},
- removeButton: {backgroundColor: '#D92800', borderColor: '#D92800', minHeight: 36, flex: 1, paddingHorizontal: 10},
- actionLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 13},
- kickLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13},
- adminTeamWrap: {gap: 8},
- adminTeamRow: {flexDirection: 'row', alignItems: 'center', gap: 8},
- adminTeamName: {flex: 1, fontSize: 14, color: '#cdd6f4', fontWeight: '600'},
- departmentName: {fontSize: 14, color: '#6c7086', fontStyle: 'italic'},
- waitlistCard: {backgroundColor: 'rgba(217, 40, 0, 0.15)', borderColor: '#D92800'},
- approveButton: {backgroundColor: '#a6e3a1', borderColor: '#a6e3a1', minHeight: 36, flex: 1, paddingHorizontal: 10},
- denyButton: {backgroundColor: '#D92800', borderColor: '#D92800', minHeight: 36, flex: 1, paddingHorizontal: 10},
+ center: { justifyContent: "center", alignItems: "center", flex: 1 },
+ container: { padding: 0 },
+ body: { color: "#bac2de", fontSize: 15 },
+ sectionTitle: {
+ fontSize: 20,
+ fontWeight: "700",
+ color: "#cdd6f4",
+ marginBottom: 12,
+ paddingHorizontal: 15,
+ paddingTop: 15,
+ },
+ openLabel: { fontSize: 13, fontWeight: "700", color: "#bd93f9" },
+ listSection: { flex: 1, paddingHorizontal: 15 },
+ emptyText: {
+ color: "#6c7086",
+ textAlign: "center",
+ marginTop: 30,
+ fontSize: 14,
+ },
+ playerRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 8,
+ marginBottom: 8,
+ },
+ playerName: { fontSize: 15, fontWeight: "600", color: "#cdd6f4", flex: 1 },
+ playerMeta: { flexDirection: "row", alignItems: "center", gap: 6 },
+ leaderBadge: { fontSize: 11, color: "#bd93f9", fontWeight: "700" },
+ teamCodeBadge: {
+ fontSize: 12,
+ fontWeight: "700",
+ color: "#a6e3a1",
+ letterSpacing: 1,
+ },
+ adminBadge: { fontSize: 12, fontWeight: "700", color: "#D92800" },
+ waitlistBadge: { fontSize: 11, fontWeight: "600", color: "#f4a460" },
+ actionRow: { flexDirection: "row", gap: 8 },
+ resetButton: {
+ backgroundColor: "#86efac",
+ borderColor: "#86efac",
+ minHeight: 36,
+ flex: 1,
+ paddingHorizontal: 10,
+ },
+ removeButton: {
+ backgroundColor: "#D92800",
+ borderColor: "#D92800",
+ minHeight: 36,
+ flex: 1,
+ paddingHorizontal: 10,
+ },
+ actionLabel: { color: "#1e1e2e", fontWeight: "600", fontSize: 13 },
+ kickLabel: { color: "#ffffff", fontWeight: "600", fontSize: 13 },
+ adminTeamWrap: { gap: 8 },
+ adminTeamRow: { flexDirection: "row", alignItems: "center", gap: 8 },
+ adminTeamName: { flex: 1, fontSize: 14, color: "#cdd6f4", fontWeight: "600" },
+ departmentName: { fontSize: 14, color: "#6c7086", fontStyle: "italic" },
+ waitlistCard: {
+ backgroundColor: "rgba(217, 40, 0, 0.15)",
+ borderColor: "#D92800",
+ },
+ approveButton: {
+ backgroundColor: "#a6e3a1",
+ borderColor: "#a6e3a1",
+ minHeight: 36,
+ flex: 1,
+ paddingHorizontal: 10,
+ },
+ denyButton: {
+ backgroundColor: "#D92800",
+ borderColor: "#D92800",
+ minHeight: 36,
+ flex: 1,
+ paddingHorizontal: 10,
+ },
});
export default PlayersScreen;
diff --git a/src/components/screens/RegisterScreen.js b/src/components/screens/RegisterScreen.js
index e58f0b1..ebb3fbc 100644
--- a/src/components/screens/RegisterScreen.js
+++ b/src/components/screens/RegisterScreen.js
@@ -1,14 +1,23 @@
-import React, {useState} from 'react';
-import {Pressable, StyleSheet, Text, TextInput, View} from 'react-native';
-import useAuthHook from '../../hooks/useAuthHook';
-import Screen from '../layout/Screen';
-import {Button, ButtonTray} from '../UI/Button';
+import { useState } from "react";
+import { Pressable, StyleSheet, Text, TextInput, View } from "react-native";
+import useAuthHook from "../../hooks/useAuthHook";
+import Screen from "../layout/Screen";
+import { Button, ButtonTray } from "../UI/Button";
-const RegisterScreen = ({navigation}) => {
+const RegisterScreen = ({ navigation }) => {
const {
- username, email, password, confirmPassword, accountType,
- error, isLoading,
- setUsername, setEmail, setPassword, setConfirmPassword, setAccountType,
+ username,
+ email,
+ password,
+ confirmPassword,
+ accountType,
+ error,
+ isLoading,
+ setUsername,
+ setEmail,
+ setPassword,
+ setConfirmPassword,
+ setAccountType,
register,
} = useAuthHook();
@@ -21,10 +30,10 @@ const RegisterScreen = ({navigation}) => {
if (!user) return;
const rootNav = navigation.getParent() || navigation;
if (user.IsAcceptedAdmin) {
- rootNav.reset({index: 0, routes: [{name: 'Admin'}]});
+ rootNav.reset({ index: 0, routes: [{ name: "Admin" }] });
return;
}
- rootNav.reset({index: 0, routes: [{name: 'Game'}]});
+ rootNav.reset({ index: 0, routes: [{ name: "Game" }] });
};
return (
@@ -71,20 +80,36 @@ const RegisterScreen = ({navigation}) => {
setAccountType('Individual')}
+ style={[
+ styles.typeButton,
+ accountType === "Individual" && styles.typeButtonActive,
+ ]}
+ onPress={() => setAccountType("Individual")}
disabled={isLoading}
>
-
+
Individual
setAccountType('Business/School')}
+ style={[
+ styles.typeButton,
+ accountType === "Business/School" && styles.typeButtonActive,
+ ]}
+ onPress={() => setAccountType("Business/School")}
disabled={isLoading}
>
-
+
Business
@@ -94,14 +119,14 @@ const RegisterScreen = ({navigation}) => {
- navigation.navigate('LoginScreen')}>
+ navigation.navigate("LoginScreen")}>
Back to Login
@@ -111,68 +136,68 @@ const RegisterScreen = ({navigation}) => {
const styles = StyleSheet.create({
container: {
- justifyContent: 'center',
+ justifyContent: "center",
},
content: {
gap: 12,
},
title: {
fontSize: 24,
- fontWeight: 'bold',
- textAlign: 'center',
+ fontWeight: "bold",
+ textAlign: "center",
marginBottom: 10,
- color: '#cdd6f4',
+ color: "#cdd6f4",
},
input: {
borderWidth: 1,
- borderColor: '#45475a',
+ borderColor: "#45475a",
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 10,
fontSize: 16,
- color: '#cdd6f4',
- backgroundColor: '#313244',
+ color: "#cdd6f4",
+ backgroundColor: "#313244",
},
typeRow: {
- flexDirection: 'row',
+ flexDirection: "row",
gap: 10,
},
typeButton: {
flex: 1,
borderWidth: 1,
- borderColor: '#45475a',
+ borderColor: "#45475a",
borderRadius: 8,
- alignItems: 'center',
+ alignItems: "center",
paddingVertical: 10,
},
typeButtonActive: {
- backgroundColor: '#bd93f9',
- borderColor: '#bd93f9',
+ backgroundColor: "#bd93f9",
+ borderColor: "#bd93f9",
},
typeText: {
- color: '#bac2de',
- fontWeight: '600',
+ color: "#bac2de",
+ fontWeight: "600",
},
typeTextActive: {
- color: '#1e1e2e',
+ color: "#1e1e2e",
},
errorText: {
- color: '#D92800',
- textAlign: 'center',
+ color: "#D92800",
+ textAlign: "center",
marginTop: 5,
},
submitButton: {
- backgroundColor: '#bd93f9',
- borderColor: '#bd93f9',
+ backgroundColor: "#bd93f9",
+ borderColor: "#bd93f9",
},
submitText: {
- color: '#1e1e2e',
- fontWeight: '600',
+ color: "#1e1e2e",
+ fontWeight: "600",
},
linkText: {
- textAlign: 'center',
- color: '#bd93f9',
- fontWeight: '600',
+ textAlign: "center",
+ color: "#bd93f9",
+ fontWeight: "600",
paddingVertical: 10,
},
});
diff --git a/src/components/screens/TeamScreen.js b/src/components/screens/TeamScreen.js
index e7f7d1a..963c932 100644
--- a/src/components/screens/TeamScreen.js
+++ b/src/components/screens/TeamScreen.js
@@ -1,20 +1,44 @@
-import React, {useEffect, useState} from 'react';
-import {ActivityIndicator, ScrollView, StyleSheet, Text, TextInput, View} from 'react-native';
-import Screen from '../layout/Screen';
-import Card from '../UI/Card';
-import {Button, ButtonTray} from '../UI/Button';
-import PendingApprovalView from '../UI/PendingApprovalView';
-import useGameHook from '../../hooks/useGameHook';
-import {getSession, setPendingAdmin, setSessionTeam, setSessionUser} from '../../hooks/SessionStore';
+import { useEffect, useState } from "react";
+import {
+ ActivityIndicator,
+ ScrollView,
+ StyleSheet,
+ Text,
+ TextInput,
+ View,
+} from "react-native";
+import Screen from "../layout/Screen";
+import Card from "../UI/Card";
+import { Button, ButtonTray } from "../UI/Button";
+import PendingApprovalView from "../UI/PendingApprovalView";
+import useGameHook from "../../hooks/useGameHook";
+import {
+ getSession,
+ setPendingAdmin,
+ setSessionTeam,
+ setSessionUser,
+} from "../../hooks/SessionStore";
-const TeamScreen = ({navigation}) => {
+const TeamScreen = ({ navigation }) => {
const session = getSession();
const isAdmin = session.isAcceptedAdmin;
const isBusiness = Boolean(session.isBusiness);
- const {getLobby, getSubgroup, getTeam, createTeam, joinTeamByCode, getTeamMembers, leaveTeam, getUser, updateUser, updateTeam, getAdminWaitlist} = useGameHook();
+ const {
+ getLobby,
+ getSubgroup,
+ getTeam,
+ createTeam,
+ joinTeamByCode,
+ getTeamMembers,
+ leaveTeam,
+ getUser,
+ updateUser,
+ updateTeam,
+ getAdminWaitlist,
+ } = useGameHook();
const [activeTid, setActiveTid] = useState(session.currentTid);
- const [teamCode, setTeamCode] = useState('');
+ const [teamCode, setTeamCode] = useState("");
const [team, setTeam] = useState(null);
const [members, setMembers] = useState([]);
const [memberNames, setMemberNames] = useState({});
@@ -22,7 +46,7 @@ const TeamScreen = ({navigation}) => {
const [teamsDisabled, setTeamsDisabled] = useState(false);
const [onAdminWaitlist, setOnAdminWaitlist] = useState(false);
const [editingName, setEditingName] = useState(false);
- const [teamNameDraft, setTeamNameDraft] = useState('');
+ const [teamNameDraft, setTeamNameDraft] = useState("");
const loadTeamData = async (tid) => {
const t = await getTeam(tid);
@@ -30,402 +54,503 @@ const TeamScreen = ({navigation}) => {
const ms = await getTeamMembers(tid);
setMembers(ms || []);
const names = {};
- for (const m of (ms || [])) {
+ for (const m of ms || []) {
const u = await getUser(m.Uid);
if (u) names[m.Uid] = u.username;
}
setMemberNames(names);
};
- useEffect(() => {
- const load = async () => {
- if (!session.currentGid) { setLoading(false); return; }
+ useEffect(() => {
+ const load = async () => {
+ if (!session.currentGid) {
+ setLoading(false);
+ return;
+ }
- const group = await getLobby(session.currentGid);
- let teamsAreEnabled = Boolean(group?.TeamsEnabled);
+ const group = await getLobby(session.currentGid);
+ let teamsAreEnabled = Boolean(group?.TeamsEnabled);
- if (isBusiness && session.currentSGid) {
- const subgroup = await getSubgroup(session.currentSGid);
- if (subgroup) teamsAreEnabled = Boolean(subgroup.TeamsEnabled);
- }
+ if (isBusiness && session.currentSGid) {
+ const subgroup = await getSubgroup(session.currentSGid);
+ if (subgroup) teamsAreEnabled = Boolean(subgroup.TeamsEnabled);
+ }
- if (session.isPendingAdmin && !session.isAcceptedAdmin) {
- const freshUser = await getUser(session.currentUid);
- if (freshUser?.IsAcceptedAdmin) {
- setPendingAdmin(false);
- setSessionUser(freshUser);
- setSessionTeam(freshUser.TGid ?? null);
- navigation.reset({index: 0, routes: [{name: 'Admin'}]});
- return;
- }
- const waitlist = await getAdminWaitlist(session.currentGid, session.currentUid);
- if (waitlist.length > 0) {
- setOnAdminWaitlist(true);
- setLoading(false);
- return;
- }
- setPendingAdmin(false);
- }
-
- if (!teamsAreEnabled) {
- setTeamsDisabled(true);
- setLoading(false);
- return;
- }
- setTeamsDisabled(false);
-
- if (!activeTid) {
- const waitlist = await getAdminWaitlist(session.currentGid, session.currentUid);
- if (waitlist.length > 0) {
- setOnAdminWaitlist(true);
- setLoading(false);
- return;
- }
- }
- if (activeTid) await loadTeamData(activeTid);
- setLoading(false);
- };
- load();
- }, [activeTid]);
-
- useEffect(() => {
- if (!onAdminWaitlist && !session.isPendingAdmin) return;
-
- let stopped = false;
- const refreshWaitlistStatus = async () => {
- const currentSession = getSession();
- if (!currentSession.currentUid || !currentSession.currentGid) return;
-
- const freshUser = await getUser(currentSession.currentUid);
- if (stopped) return;
-
- if (freshUser?.IsAcceptedAdmin) {
- setPendingAdmin(false);
- setSessionUser(freshUser);
- setSessionTeam(freshUser.TGid ?? null);
- navigation.reset({index: 0, routes: [{name: 'Admin'}]});
- return;
- }
-
- const waitlist = await getAdminWaitlist(currentSession.currentGid, currentSession.currentUid);
- if (stopped) return;
-
- const isStillWaiting = waitlist.length > 0;
- setOnAdminWaitlist(isStillWaiting);
- if (!isStillWaiting) {
- setPendingAdmin(false);
- }
- };
-
- refreshWaitlistStatus();
- const intervalId = setInterval(refreshWaitlistStatus, 5000);
-
- return () => {
- stopped = true;
- clearInterval(intervalId);
- };
- }, [onAdminWaitlist, session.isPendingAdmin]);
-
- const handleJoinTeam = async () => {
- if (!teamCode.trim()) return;
- // Force uppercase just in case a lowercase code is pasted
- const code = teamCode.trim().toUpperCase();
- const result = await joinTeamByCode({
- JoinCode: code,
- Uid: session.currentUid,
- ExpectedSGid: session.currentSGid,
- });
- if (!result) return;
-
- // If the server indicates this code matched the admin join code
- if (result.adminWaitlist) {
- setPendingAdmin(true);
- setOnAdminWaitlist(true);
- return;
+ if (session.isPendingAdmin && !session.isAcceptedAdmin) {
+ const freshUser = await getUser(session.currentUid);
+ if (freshUser?.IsAcceptedAdmin) {
+ setPendingAdmin(false);
+ setSessionUser(freshUser);
+ setSessionTeam(freshUser.TGid ?? null);
+ navigation.reset({ index: 0, routes: [{ name: "Admin" }] });
+ return;
}
-
- setSessionTeam(result.Tid);
- setActiveTid(result.Tid);
- };
-
- const handleCreateTeam = async () => {
- if (!session.currentGid) return;
- const result = await createTeam({Gid: session.currentGid, SGid: session.currentSGid, TeamName: 'My Team'});
- if (!result) return;
- const membership = await joinTeamByCode({Tid: result.Tid, Uid: session.currentUid});
- if (membership) {
- setSessionTeam(result.Tid);
- setActiveTid(result.Tid);
+ const waitlist = await getAdminWaitlist(
+ session.currentGid,
+ session.currentUid,
+ );
+ if (waitlist.length > 0) {
+ setOnAdminWaitlist(true);
+ setLoading(false);
+ return;
}
- };
+ setPendingAdmin(false);
+ }
- const handleLeaveTeam = async () => {
- const myMembership = members.find((m) => m.Uid === session.currentUid);
- if (!myMembership) return;
- await leaveTeam(myMembership.id);
- await updateUser(session.currentUid, {TGid: null});
- setSessionTeam(null);
- setActiveTid(null);
- setTeam(null);
- setMembers([]);
- setMemberNames({});
- };
+ if (!teamsAreEnabled) {
+ setTeamsDisabled(true);
+ setLoading(false);
+ return;
+ }
+ setTeamsDisabled(false);
- const handleKickMember = async (member) => {
- await leaveTeam(member.id);
- await loadTeamData(activeTid);
- };
-
- const handleRenameTeam = async () => {
- const trimmed = teamNameDraft.trim();
- if (!trimmed || !activeTid) return;
- const result = await updateTeam(activeTid, {TeamName: trimmed});
- if (result) {
- setTeam(result);
- setEditingName(false);
+ if (!activeTid) {
+ const waitlist = await getAdminWaitlist(
+ session.currentGid,
+ session.currentUid,
+ );
+ if (waitlist.length > 0) {
+ setOnAdminWaitlist(true);
+ setLoading(false);
+ return;
}
+ }
+ if (activeTid) await loadTeamData(activeTid);
+ setLoading(false);
+ };
+ load();
+ }, [activeTid]);
+
+ useEffect(() => {
+ if (!onAdminWaitlist && !session.isPendingAdmin) return;
+
+ let stopped = false;
+ const refreshWaitlistStatus = async () => {
+ const currentSession = getSession();
+ if (!currentSession.currentUid || !currentSession.currentGid) return;
+
+ const freshUser = await getUser(currentSession.currentUid);
+ if (stopped) return;
+
+ if (freshUser?.IsAcceptedAdmin) {
+ setPendingAdmin(false);
+ setSessionUser(freshUser);
+ setSessionTeam(freshUser.TGid ?? null);
+ navigation.reset({ index: 0, routes: [{ name: "Admin" }] });
+ return;
+ }
+
+ const waitlist = await getAdminWaitlist(
+ currentSession.currentGid,
+ currentSession.currentUid,
+ );
+ if (stopped) return;
+
+ const isStillWaiting = waitlist.length > 0;
+ setOnAdminWaitlist(isStillWaiting);
+ if (!isStillWaiting) {
+ setPendingAdmin(false);
+ }
};
- // Check if current user is the team leader
+ refreshWaitlistStatus();
+ const intervalId = setInterval(refreshWaitlistStatus, 5000);
+
+ return () => {
+ stopped = true;
+ clearInterval(intervalId);
+ };
+ }, [onAdminWaitlist, session.isPendingAdmin]);
+
+ const handleJoinTeam = async () => {
+ if (!teamCode.trim()) return;
+ // Force uppercase just in case a lowercase code is pasted
+ const code = teamCode.trim().toUpperCase();
+ const result = await joinTeamByCode({
+ JoinCode: code,
+ Uid: session.currentUid,
+ ExpectedSGid: session.currentSGid,
+ });
+ if (!result) return;
+
+ // If the server indicates this code matched the admin join code
+ if (result.adminWaitlist) {
+ setPendingAdmin(true);
+ setOnAdminWaitlist(true);
+ return;
+ }
+
+ setSessionTeam(result.Tid);
+ setActiveTid(result.Tid);
+ };
+
+ const handleCreateTeam = async () => {
+ if (!session.currentGid) return;
+ const result = await createTeam({
+ Gid: session.currentGid,
+ SGid: session.currentSGid,
+ TeamName: "My Team",
+ });
+ if (!result) return;
+ const membership = await joinTeamByCode({
+ Tid: result.Tid,
+ Uid: session.currentUid,
+ });
+ if (membership) {
+ setSessionTeam(result.Tid);
+ setActiveTid(result.Tid);
+ }
+ };
+
+ const handleLeaveTeam = async () => {
const myMembership = members.find((m) => m.Uid === session.currentUid);
- const isCurrentUserLeader = Boolean(myMembership?.IsLeader);
+ if (!myMembership) return;
+ await leaveTeam(myMembership.id);
+ await updateUser(session.currentUid, { TGid: null });
+ setSessionTeam(null);
+ setActiveTid(null);
+ setTeam(null);
+ setMembers([]);
+ setMemberNames({});
+ };
-// View -----------------------
+ const handleKickMember = async (member) => {
+ await leaveTeam(member.id);
+ await loadTeamData(activeTid);
+ };
- if (loading) {
- return (
-
-
-
- );
+ const handleRenameTeam = async () => {
+ const trimmed = teamNameDraft.trim();
+ if (!trimmed || !activeTid) return;
+ const result = await updateTeam(activeTid, { TeamName: trimmed });
+ if (result) {
+ setTeam(result);
+ setEditingName(false);
}
+ };
- if (teamsDisabled) {
- return (
-
-
- Teams Disabled
- The admin has not enabled teams for this game.
-
-
- );
- }
+ // Check if current user is the team leader
+ const myMembership = members.find((m) => m.Uid === session.currentUid);
+ const isCurrentUserLeader = Boolean(myMembership?.IsLeader);
- // Waiting for admin approval
- if (onAdminWaitlist) {
- return ;
- }
+ // View -----------------------
- // Not in a team
- if (!activeTid || !team) {
- // Disable team actions until the user has joined a game
- const notInGame = !session.currentGid;
-
- return (
-
- {notInGame && (
- Join a game first to manage teams.
- )}
-
- setTeamCode(text.toUpperCase())}
- autoCapitalize="characters"
- editable={!notInGame}
- />
-
-
- {!isAdmin && (
-
-
-
-
-
- )}
-
- );
- }
-
- // In a team
+ if (loading) {
return (
-
-
- Team Code:
- {team.JoinCode}
-
-
- {/* Team name display / edit */}
-
- {editingName ? (
-
-
-
- setEditingName(false)}
- styleButton={styles.renameCancelButton}
- styleLabel={styles.renameCancelLabel}
- />
-
- ) : (
-
- {team.TeamName || 'Unnamed Team'}
- {isCurrentUserLeader && (
- { setTeamNameDraft(team.TeamName || ''); setEditingName(true); }}
- styleButton={styles.renameButton}
- styleLabel={styles.renameLabel}
- />
- )}
-
- )}
-
-
-
- {members.map((m) => (
- // Highlight the current user's card so they can identify themselves
-
-
-
-
- {memberNames[m.Uid] || `Player ${m.Uid}`}
-
- {m.IsLeader && (Team Leader)}
-
- {isCurrentUserLeader && m.Uid !== session.currentUid && (
- handleKickMember(m)}
- styleButton={styles.kickButton}
- styleLabel={styles.kickLabel}
- />
- )}
-
-
- ))}
- {members.length === 0 && (
- No members yet.
- )}
- {/* Leave button lives inside the scroll so it is never obscured by the bottom nav bar */}
-
-
-
-
-
+
+
+
);
+ }
+
+ if (teamsDisabled) {
+ return (
+
+
+ Teams Disabled
+
+ The admin has not enabled teams for this game.
+
+
+
+ );
+ }
+
+ // Waiting for admin approval
+ if (onAdminWaitlist) {
+ return (
+
+ );
+ }
+
+ // Not in a team
+ if (!activeTid || !team) {
+ // Disable team actions until the user has joined a game
+ const notInGame = !session.currentGid;
+
+ return (
+
+ {notInGame && (
+
+ Join a game first to manage teams.
+
+ )}
+
+ setTeamCode(text.toUpperCase())}
+ autoCapitalize="characters"
+ editable={!notInGame}
+ />
+
+
+ {!isAdmin && (
+
+
+
+
+
+ )}
+
+ );
+ }
+
+ // In a team
+ return (
+
+
+ Team Code:
+ {team.JoinCode}
+
+
+ {/* Team name display / edit */}
+
+ {editingName ? (
+
+
+
+ setEditingName(false)}
+ styleButton={styles.renameCancelButton}
+ styleLabel={styles.renameCancelLabel}
+ />
+
+ ) : (
+
+
+ {team.TeamName || "Unnamed Team"}
+
+ {isCurrentUserLeader && (
+ {
+ setTeamNameDraft(team.TeamName || "");
+ setEditingName(true);
+ }}
+ styleButton={styles.renameButton}
+ styleLabel={styles.renameLabel}
+ />
+ )}
+
+ )}
+
+
+
+ {members.map((m) => (
+ // Highlight the current user's card so they can identify themselves
+
+
+
+
+ {memberNames[m.Uid] || `Player ${m.Uid}`}
+
+ {m.IsLeader && (
+ (Team Leader)
+ )}
+
+ {isCurrentUserLeader && m.Uid !== session.currentUid && (
+ handleKickMember(m)}
+ styleButton={styles.kickButton}
+ styleLabel={styles.kickLabel}
+ />
+ )}
+
+
+ ))}
+ {members.length === 0 && (
+ No members yet.
+ )}
+ {/* Leave button lives inside the scroll so it is never obscured by the bottom nav bar */}
+
+
+
+
+
+ );
};
const styles = StyleSheet.create({
- center: {justifyContent: 'center', alignItems: 'center'},
- container: {padding: 0},
- disabledWrap: {paddingHorizontal: 30, alignItems: 'center', opacity: 0.45},
- disabledTitle: {fontSize: 20, fontWeight: '700', color: '#ffffff', marginBottom: 10, textAlign: 'center'},
- disabledBody: {fontSize: 15, color: '#ffffff', textAlign: 'center'},
- notInGameText: {color: '#ffffff', fontSize: 15, fontWeight: '600', marginBottom: 16, textAlign: 'center', paddingHorizontal: 20},
- inputDisabled: {backgroundColor: '#313244', opacity: 0.5},
- inputRow: {flexDirection: 'row', gap: 10, marginBottom: 15, width: '100%', paddingHorizontal: 20},
- fullRow: {width: '100%', paddingHorizontal: 20},
- codeInput: {
- flex: 1,
- borderWidth: 1,
- borderColor: '#45475a',
- borderRadius: 8,
- paddingHorizontal: 12,
- paddingVertical: 10,
- fontSize: 16,
- color: '#ffffff',
- backgroundColor: '#313244',
- },
- joinButton: {backgroundColor: '#bd93f9', borderColor: '#bd93f9', flex: 0, paddingHorizontal: 20},
- joinLabel: {color: '#1e1e2e', fontWeight: '600'},
- createButton: {backgroundColor: '#a6e3a1', borderColor: '#a6e3a1'},
- createLabel: {color: '#1e1e2e', fontWeight: '600'},
- waitlistText: {
- fontSize: 18,
- fontWeight: '700',
- color: '#ffffff',
- textAlign: 'center',
- paddingHorizontal: 30,
- },
- teamHeader: {
- backgroundColor: '#45475a',
- paddingVertical: 14,
- paddingHorizontal: 16,
- flexDirection: 'row',
- alignItems: 'center',
- gap: 8,
- },
- teamCodeLabel: {color: '#ffffff', fontSize: 15, fontWeight: '600'},
- teamCodeValue: {color: '#ffffff', fontSize: 17, fontWeight: '700', letterSpacing: 2},
- teamNameSection: {paddingHorizontal: 15, paddingVertical: 10, borderBottomWidth: 1, borderBottomColor: '#45475a'},
- teamNameText: {fontSize: 18, fontWeight: '700', color: '#ffffff', flex: 1},
- renameRow: {flexDirection: 'row', alignItems: 'center', gap: 8},
- renameInput: {
- flex: 1,
- borderWidth: 1,
- borderColor: '#45475a',
- borderRadius: 8,
- paddingHorizontal: 12,
- paddingVertical: 8,
- fontSize: 16,
- color: '#ffffff',
- backgroundColor: '#313244',
- },
- renameButton: {backgroundColor: '#bd93f9', borderColor: '#bd93f9', flex: 0, minHeight: 36, paddingHorizontal: 14},
- renameLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 13},
- renameSaveButton: {backgroundColor: '#a6e3a1', borderColor: '#a6e3a1', flex: 0, minHeight: 36, paddingHorizontal: 12},
- renameSaveLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 13},
- renameCancelButton: {backgroundColor: '#6c7086', borderColor: '#6c7086', flex: 0, minHeight: 36, paddingHorizontal: 12},
- renameCancelLabel: {color: '#cdd6f4', fontWeight: '600', fontSize: 13},
- memberList: {flex: 1, paddingHorizontal: 15, paddingTop: 10},
- memberRow: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- },
- memberInfo: {flex: 1, flexDirection: 'row', alignItems: 'center', gap: 6},
- memberName: {fontSize: 16, color: '#ffffff', fontWeight: '600'},
- leaderBadge: {fontSize: 12, color: '#bd93f9', fontWeight: '700'},
- kickButton: {backgroundColor: '#D92800', borderColor: '#D92800', minHeight: 36, flex: 0, paddingHorizontal: 14},
- kickLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 13},
- emptyText: {color: '#ffffff', textAlign: 'center', marginTop: 30, fontSize: 14},
- leaveWrap: {paddingHorizontal: 0, paddingTop: 10, paddingBottom: 16},
- leaveButton: {backgroundColor: '#D92800', borderColor: '#D92800'},
- leaveLabel: {color: '#1e1e2e', fontWeight: '600'},
- selfCard: {backgroundColor: 'rgba(166, 227, 161, 0.15)', borderColor: '#a6e3a1'},
+ center: { justifyContent: "center", alignItems: "center" },
+ container: { padding: 0 },
+ disabledWrap: { paddingHorizontal: 30, alignItems: "center", opacity: 0.45 },
+ disabledTitle: {
+ fontSize: 20,
+ fontWeight: "700",
+ color: "#ffffff",
+ marginBottom: 10,
+ textAlign: "center",
+ },
+ disabledBody: { fontSize: 15, color: "#ffffff", textAlign: "center" },
+ notInGameText: {
+ color: "#ffffff",
+ fontSize: 15,
+ fontWeight: "600",
+ marginBottom: 16,
+ textAlign: "center",
+ paddingHorizontal: 20,
+ },
+ inputDisabled: { backgroundColor: "#313244", opacity: 0.5 },
+ inputRow: {
+ flexDirection: "row",
+ gap: 10,
+ marginBottom: 15,
+ width: "100%",
+ paddingHorizontal: 20,
+ },
+ fullRow: { width: "100%", paddingHorizontal: 20 },
+ codeInput: {
+ flex: 1,
+ borderWidth: 1,
+ borderColor: "#45475a",
+ borderRadius: 8,
+ paddingHorizontal: 12,
+ paddingVertical: 10,
+ fontSize: 16,
+ color: "#ffffff",
+ backgroundColor: "#313244",
+ },
+ joinButton: {
+ backgroundColor: "#bd93f9",
+ borderColor: "#bd93f9",
+ flex: 0,
+ paddingHorizontal: 20,
+ },
+ joinLabel: { color: "#1e1e2e", fontWeight: "600" },
+ createButton: { backgroundColor: "#a6e3a1", borderColor: "#a6e3a1" },
+ createLabel: { color: "#1e1e2e", fontWeight: "600" },
+ waitlistText: {
+ fontSize: 18,
+ fontWeight: "700",
+ color: "#ffffff",
+ textAlign: "center",
+ paddingHorizontal: 30,
+ },
+ teamHeader: {
+ backgroundColor: "#45475a",
+ paddingVertical: 14,
+ paddingHorizontal: 16,
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 8,
+ },
+ teamCodeLabel: { color: "#ffffff", fontSize: 15, fontWeight: "600" },
+ teamCodeValue: {
+ color: "#ffffff",
+ fontSize: 17,
+ fontWeight: "700",
+ letterSpacing: 2,
+ },
+ teamNameSection: {
+ paddingHorizontal: 15,
+ paddingVertical: 10,
+ borderBottomWidth: 1,
+ borderBottomColor: "#45475a",
+ },
+ teamNameText: { fontSize: 18, fontWeight: "700", color: "#ffffff", flex: 1 },
+ renameRow: { flexDirection: "row", alignItems: "center", gap: 8 },
+ renameInput: {
+ flex: 1,
+ borderWidth: 1,
+ borderColor: "#45475a",
+ borderRadius: 8,
+ paddingHorizontal: 12,
+ paddingVertical: 8,
+ fontSize: 16,
+ color: "#ffffff",
+ backgroundColor: "#313244",
+ },
+ renameButton: {
+ backgroundColor: "#bd93f9",
+ borderColor: "#bd93f9",
+ flex: 0,
+ minHeight: 36,
+ paddingHorizontal: 14,
+ },
+ renameLabel: { color: "#1e1e2e", fontWeight: "600", fontSize: 13 },
+ renameSaveButton: {
+ backgroundColor: "#a6e3a1",
+ borderColor: "#a6e3a1",
+ flex: 0,
+ minHeight: 36,
+ paddingHorizontal: 12,
+ },
+ renameSaveLabel: { color: "#1e1e2e", fontWeight: "600", fontSize: 13 },
+ renameCancelButton: {
+ backgroundColor: "#6c7086",
+ borderColor: "#6c7086",
+ flex: 0,
+ minHeight: 36,
+ paddingHorizontal: 12,
+ },
+ renameCancelLabel: { color: "#cdd6f4", fontWeight: "600", fontSize: 13 },
+ memberList: { flex: 1, paddingHorizontal: 15, paddingTop: 10 },
+ memberRow: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "center",
+ },
+ memberInfo: { flex: 1, flexDirection: "row", alignItems: "center", gap: 6 },
+ memberName: { fontSize: 16, color: "#ffffff", fontWeight: "600" },
+ leaderBadge: { fontSize: 12, color: "#bd93f9", fontWeight: "700" },
+ kickButton: {
+ backgroundColor: "#D92800",
+ borderColor: "#D92800",
+ minHeight: 36,
+ flex: 0,
+ paddingHorizontal: 14,
+ },
+ kickLabel: { color: "#1e1e2e", fontWeight: "600", fontSize: 13 },
+ emptyText: {
+ color: "#ffffff",
+ textAlign: "center",
+ marginTop: 30,
+ fontSize: 14,
+ },
+ leaveWrap: { paddingHorizontal: 0, paddingTop: 10, paddingBottom: 16 },
+ leaveButton: { backgroundColor: "#D92800", borderColor: "#D92800" },
+ leaveLabel: { color: "#1e1e2e", fontWeight: "600" },
+ selfCard: {
+ backgroundColor: "rgba(166, 227, 161, 0.15)",
+ borderColor: "#a6e3a1",
+ },
});
export default TeamScreen;
diff --git a/src/components/screens/global/GlobalMapScreen.js b/src/components/screens/global/GlobalMapScreen.js
index 99e872e..7a3bbcd 100644
--- a/src/components/screens/global/GlobalMapScreen.js
+++ b/src/components/screens/global/GlobalMapScreen.js
@@ -1,16 +1,16 @@
-import {useCallback, useEffect, useMemo, useRef, useState} from "react";
-import {ActivityIndicator, StyleSheet, Text, View} from "react-native";
-import MapView, {Circle, Marker, Polygon} from "react-native-maps";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { ActivityIndicator, StyleSheet, Text, View } from "react-native";
+import MapView, { Circle, Marker, Polygon } from "react-native-maps";
import * as Location from "expo-location";
import Screen from "../../layout/Screen";
-import {Button, ButtonTray} from "../../UI/Button";
+import { Button, ButtonTray } from "../../UI/Button";
import ClaimTimerView from "../../gameplay/ClaimTimerView";
import CacheList from "../../../entity/cache/CacheList";
import useGlobalHook from "../../../hooks/useGlobalHook";
import usePlayerGame from "../../../hooks/usePlayerGame";
-import {getSession} from "../../../hooks/SessionStore";
-import {getFovCone, isInClaimCone} from "../../../utils/geoMath";
-import {GAME_MODE} from "../../../utils/gameConstants";
+import { getSession } from "../../../hooks/SessionStore";
+import { getFovCone, isInClaimCone } from "../../../utils/geoMath";
+import { GAME_MODE } from "../../../utils/gameConstants";
const DEFAULT_REGION = {
latitude: 51.5074,
@@ -27,17 +27,19 @@ const GlobalMapScreen = ({ navigation, route }) => {
const session = getSession();
const routeEvent = route?.params?.event ?? null;
const eventId =
- route?.params?.eventId ?? routeEvent?.EventID ?? session.currentGlobalEventId;
- const { getCachesByEvent, getFindsByPlayer, logFind } = useGlobalHook();
+ route?.params?.eventId ??
+ routeEvent?.EventID ??
+ session.currentGlobalEventId;
+ const { getCachesByEvent, getFindsByPlayer, claimCache } = useGlobalHook();
const globalApiRef = useRef({
getCachesByEvent,
getFindsByPlayer,
- logFind,
+ claimCache,
});
globalApiRef.current = {
getCachesByEvent,
getFindsByPlayer,
- logFind,
+ claimCache,
};
// State -------------------------------
@@ -53,7 +55,8 @@ const GlobalMapScreen = ({ navigation, route }) => {
route?.params?.selectedCacheId ?? route?.params?.cache?.CacheID ?? null,
);
const [claimedPopupVisible, setClaimedPopupVisible] = useState(false);
- const [claimedPopupMessage, setClaimedPopupMessage] = useState("Cache Claimed!");
+ const [claimedPopupMessage, setClaimedPopupMessage] =
+ useState("Cache Claimed!");
const claimableCaches = useMemo(
() =>
@@ -126,24 +129,29 @@ const GlobalMapScreen = ({ navigation, route }) => {
(cache) => String(cache.CacheID) === String(cacheId),
);
- const result = await globalApiRef.current.logFind({
- FindPlayerID: session.currentGlobalPlayerId,
- FindCacheID: claimedCache?.CacheID ?? cacheId,
- FindDatetime: new Date().toISOString(),
- });
+ const result = await globalApiRef.current.claimCache(
+ session.currentGlobalPlayerId,
+ claimedCache,
+ );
setIsClaiming(false);
- if (result) {
- const cacheName = claimedCache?.CacheName || "Cache";
- const points = Number(claimedCache?.CachePoints ?? 0);
- setClaimedPopupMessage(`${cacheName} claimed! You gained +${points} points.`);
- setClaimedPopupVisible(true);
- setTimeout(() => setClaimedPopupVisible(false), 3000);
- await loadData({ forceRefresh: true });
+ if (!result || result.error) {
+ setError(`Failed to claim cache: ${result?.error || "Unknown error"}`);
+ return;
}
+
+ // Optimistically mark as found — no loadData() to avoid overwriting
+ // this state with a stale server response.
+ setFoundCacheIds((prev) => [...prev, String(cacheId)]);
+ const cacheName = claimedCache?.CacheName || "Cache";
+ setClaimedPopupMessage(
+ `${cacheName} claimed! You gained +${result.points} points.`,
+ );
+ setClaimedPopupVisible(true);
+ setTimeout(() => setClaimedPopupVisible(false), 3000);
},
- [caches, foundCacheIds, loadData, session.currentGlobalPlayerId, setIsClaiming],
+ [caches, foundCacheIds, session.currentGlobalPlayerId, setIsClaiming],
);
const handleCacheSelect = (cache) => {
@@ -234,7 +242,9 @@ const GlobalMapScreen = ({ navigation, route }) => {
useEffect(() => {
if (selectedCacheId) return;
- setSelectedCacheId(route?.params?.cache?.CacheID ?? claimableCaches[0]?.id ?? null);
+ setSelectedCacheId(
+ route?.params?.cache?.CacheID ?? claimableCaches[0]?.id ?? null,
+ );
}, [claimableCaches, route?.params?.cache?.CacheID, selectedCacheId]);
useEffect(() => {
@@ -335,33 +345,32 @@ const GlobalMapScreen = ({ navigation, route }) => {
region={mapRegion}
showsUserLocation
>
- {(caches || []).map((cache) => {
- const found = foundCacheIds.includes(String(cache.CacheID));
- const selected = selectedCacheId === cache.CacheID;
- return (
- setSelectedCacheId(cache.CacheID)}
- onCalloutPress={() =>
- navigation.navigate("GlobalCacheViewScreen", {
- cache,
- event: routeEvent,
- eventId,
- isFound: found,
- })
- }
- />
- );
- })}
+ {(caches || [])
+ .filter((cache) => !foundCacheIds.includes(String(cache.CacheID)))
+ .map((cache) => {
+ const selected = selectedCacheId === cache.CacheID;
+ return (
+ setSelectedCacheId(cache.CacheID)}
+ onCalloutPress={() =>
+ navigation.navigate("GlobalCacheViewScreen", {
+ cache,
+ event: routeEvent,
+ eventId,
+ isFound: false,
+ })
+ }
+ />
+ );
+ })}
{claimTarget ? (
{
) : (
!foundCacheIds.includes(String(cache.CacheID)),
+ )}
foundCacheIds={foundCacheIds}
onSelect={handleCacheSelect}
/>
diff --git a/src/hooks/useGlobalHook.js b/src/hooks/useGlobalHook.js
index 8492640..bf7cd75 100644
--- a/src/hooks/useGlobalHook.js
+++ b/src/hooks/useGlobalHook.js
@@ -1,12 +1,6 @@
-import {useCallback, useMemo} from "react";
+import { useCallback, useMemo } from "react";
import API from "../components/API/API";
import globalApiConfig from "../components/API/api.json";
-import {
- clearGlobalDataCache,
- clearGlobalDataCacheByPrefix,
- getOrFetchGlobalData,
- writeGlobalDataCache,
-} from "../utils/globalDataCache";
const GLOBAL_BASE = String(
process.env.EXPO_PUBLIC_GLOBAL_API_BASE ||
@@ -24,26 +18,6 @@ const DEFAULT_GLOBAL_PROFILE_IMAGE_URL =
const isGlobalApiReady = () => Boolean(GLOBAL_BASE);
-const CACHE_TTL = {
- events: 20000,
- players: 12000,
- caches: 20000,
- finds: 8000,
- users: 15000,
-};
-
-const CACHE_KEY = {
- publicEvents: "global:events:public",
- users: "global:users",
- event: (eventId) => `global:event:${eventId}`,
- playersByEvent: (eventId) => `global:players:event:${eventId}`,
- cachesByEvent: (eventId) => `global:caches:event:${eventId}`,
- cacheById: (cacheId) => `global:cache:${cacheId}`,
- findsByEvent: (eventId) => `global:finds:event:${eventId}`,
- findsByPlayer: (playerId) => `global:finds:player:${playerId}`,
- userById: (userId) => `global:user:${userId}`,
-};
-
// Appends the API key as a query param when provided.
const withKey = (url) => {
if (!GLOBAL_KEY) return url;
@@ -155,56 +129,33 @@ const useGlobalHook = () => {
// Handlers ----------------------------
// Events
- const getPublicEvents = useCallback(
- async (options = {}) => {
- if (!isGlobalApiReady()) return [];
- return getOrFetchGlobalData(
- CACHE_KEY.publicEvents,
- async () => {
- const response = await API.get(withKey(endpoints.events));
- if (!response.isSuccess) return [];
- const rows = unwrapList(response.result);
- return rows.filter((e) => e.EventIspublic);
- },
- { ttlMs: CACHE_TTL.events, forceRefresh: options.forceRefresh },
- );
- },
- [endpoints.events],
- );
+ const getPublicEvents = useCallback(async () => {
+ if (!isGlobalApiReady()) return [];
+ const response = await API.get(withKey(endpoints.events));
+ if (!response.isSuccess) return [];
+ const rows = unwrapList(response.result);
+ return rows.filter((e) => e.EventIspublic);
+ }, [endpoints.events]);
const getEvent = useCallback(
- async (eventId, options = {}) => {
+ async (eventId) => {
if (!isGlobalApiReady()) return null;
- return getOrFetchGlobalData(
- CACHE_KEY.event(eventId),
- async () => {
- const response = await API.get(
- withKey(`${endpoints.events}/${eventId}`),
- );
- if (!response.isSuccess) return null;
- return unwrapSingle(response.result);
- },
- { ttlMs: CACHE_TTL.events, forceRefresh: options.forceRefresh },
- );
+ const response = await API.get(withKey(`${endpoints.events}/${eventId}`));
+ if (!response.isSuccess) return null;
+ return unwrapSingle(response.result);
},
[endpoints.events],
);
// Players
const getPlayersByEvent = useCallback(
- async (eventId, options = {}) => {
+ async (eventId) => {
if (!isGlobalApiReady()) return [];
- return getOrFetchGlobalData(
- CACHE_KEY.playersByEvent(eventId),
- async () => {
- const response = await API.get(
- withKey(`${endpoints.players}/events/${eventId}`),
- );
- if (!response.isSuccess) return [];
- return unwrapList(response.result);
- },
- { ttlMs: CACHE_TTL.players, forceRefresh: options.forceRefresh },
+ const response = await API.get(
+ withKey(`${endpoints.players}/events/${eventId}`),
);
+ if (!response.isSuccess) return [];
+ return unwrapList(response.result);
},
[endpoints.players],
);
@@ -214,86 +165,55 @@ const useGlobalHook = () => {
if (!isGlobalApiReady()) return null;
const response = await API.post(withKey(endpoints.players), data);
if (!response.isSuccess) return null;
- const joined = unwrapSingle(response.result);
- const eventId = data?.PlayerEventID ?? joined?.PlayerEventID;
- if (eventId !== undefined && eventId !== null) {
- clearGlobalDataCache(CACHE_KEY.playersByEvent(eventId));
- }
- return joined;
+ return unwrapSingle(response.result);
},
[endpoints.players],
);
// Caches
const getCachesByEvent = useCallback(
- async (eventId, options = {}) => {
+ async (eventId) => {
if (!isGlobalApiReady()) return [];
- return getOrFetchGlobalData(
- CACHE_KEY.cachesByEvent(eventId),
- async () => {
- const response = await API.get(
- withKey(`${endpoints.caches}/events/${eventId}`),
- );
- if (!response.isSuccess) return [];
- return unwrapList(response.result);
- },
- { ttlMs: CACHE_TTL.caches, forceRefresh: options.forceRefresh },
+ const response = await API.get(
+ withKey(`${endpoints.caches}/events/${eventId}`),
);
+ if (!response.isSuccess) return [];
+ return unwrapList(response.result);
},
[endpoints.caches],
);
const getCache = useCallback(
- async (cacheId, options = {}) => {
+ async (cacheId) => {
if (!isGlobalApiReady()) return null;
- return getOrFetchGlobalData(
- CACHE_KEY.cacheById(cacheId),
- async () => {
- const response = await API.get(
- withKey(`${endpoints.caches}/${cacheId}`),
- );
- if (!response.isSuccess) return null;
- return unwrapSingle(response.result);
- },
- { ttlMs: CACHE_TTL.caches, forceRefresh: options.forceRefresh },
- );
+ const response = await API.get(withKey(`${endpoints.caches}/${cacheId}`));
+ if (!response.isSuccess) return null;
+ return unwrapSingle(response.result);
},
[endpoints.caches],
);
// Finds
const getFindsByEvent = useCallback(
- async (eventId, options = {}) => {
+ async (eventId) => {
if (!isGlobalApiReady()) return [];
- return getOrFetchGlobalData(
- CACHE_KEY.findsByEvent(eventId),
- async () => {
- const response = await API.get(
- withKey(`${endpoints.finds}/events/${eventId}`),
- );
- if (!response.isSuccess) return [];
- return unwrapList(response.result);
- },
- { ttlMs: CACHE_TTL.finds, forceRefresh: options.forceRefresh },
+ const response = await API.get(
+ withKey(`${endpoints.finds}/events/${eventId}`),
);
+ if (!response.isSuccess) return [];
+ return unwrapList(response.result);
},
[endpoints.finds],
);
const getFindsByPlayer = useCallback(
- async (playerId, options = {}) => {
+ async (playerId) => {
if (!isGlobalApiReady()) return [];
- return getOrFetchGlobalData(
- CACHE_KEY.findsByPlayer(playerId),
- async () => {
- const response = await API.get(
- withKey(`${endpoints.finds}/players/${playerId}`),
- );
- if (!response.isSuccess) return [];
- return unwrapList(response.result);
- },
- { ttlMs: CACHE_TTL.finds, forceRefresh: options.forceRefresh },
+ const response = await API.get(
+ withKey(`${endpoints.finds}/players/${playerId}`),
);
+ if (!response.isSuccess) return [];
+ return unwrapList(response.result);
},
[endpoints.finds],
);
@@ -303,57 +223,53 @@ const useGlobalHook = () => {
if (!isGlobalApiReady()) return null;
const response = await API.post(withKey(endpoints.finds), data);
if (!response.isSuccess) return null;
- const find = unwrapSingle(response.result) || { ...data };
- if (data?.FindPlayerID !== undefined && data?.FindPlayerID !== null) {
- clearGlobalDataCache(CACHE_KEY.findsByPlayer(data.FindPlayerID));
- }
- // Event ID is not always present in payload, so clear all event-level find caches.
- clearGlobalDataCacheByPrefix("global:finds:event:");
- return find;
+ return unwrapSingle(response.result) || { ...data };
+ },
+ [endpoints.finds],
+ );
+
+ const claimCache = useCallback(
+ async (playerId, cache) => {
+ if (!isGlobalApiReady() || !playerId || !cache) return null;
+ const response = await API.post(withKey(endpoints.finds), {
+ FindPlayerID: playerId,
+ FindCacheID: cache.CacheID,
+ FindDatetime: new Date().toISOString(),
+ FindImageURL: cache.CacheImageURL || DEFAULT_GLOBAL_PROFILE_IMAGE_URL,
+ });
+ if (!response.isSuccess)
+ return { error: response.message || "Unknown error" };
+ const find = unwrapSingle(response.result) || {
+ FindPlayerID: playerId,
+ FindCacheID: cache.CacheID,
+ };
+ return { find, points: Number(cache.CachePoints ?? 0) };
},
[endpoints.finds],
);
// Users
- const getUsers = useCallback(
- async (options = {}) => {
- if (!isGlobalApiReady()) return [];
- return getOrFetchGlobalData(
- CACHE_KEY.users,
- async () => {
- const response = await API.get(withKey(endpoints.users));
- if (!response.isSuccess) return [];
- return unwrapList(response.result);
- },
- { ttlMs: CACHE_TTL.users, forceRefresh: options.forceRefresh },
- );
- },
- [endpoints.users],
- );
+ const getUsers = useCallback(async () => {
+ if (!isGlobalApiReady()) return [];
+ const response = await API.get(withKey(endpoints.users));
+ if (!response.isSuccess) return [];
+ return unwrapList(response.result);
+ }, [endpoints.users]);
const getUser = useCallback(
- async (userId, options = {}) => {
+ async (userId) => {
if (!isGlobalApiReady()) return null;
-
- const key = CACHE_KEY.userById(userId);
- return getOrFetchGlobalData(
- key,
- async () => {
- // Some global API deployments expose list-only user routes.
- const byIdResponse = await API.get(
- withKey(`${endpoints.users}/${userId}`),
- );
- if (byIdResponse.isSuccess) {
- const single = unwrapSingle(byIdResponse.result);
- if (single) return single;
- }
-
- const users = await getUsers({ forceRefresh: options.forceRefresh });
- return (
- users.find((user) => String(user.UserID) === String(userId)) || null
- );
- },
- { ttlMs: CACHE_TTL.users, forceRefresh: options.forceRefresh },
+ // Some global API deployments expose list-only user routes.
+ const byIdResponse = await API.get(
+ withKey(`${endpoints.users}/${userId}`),
+ );
+ if (byIdResponse.isSuccess) {
+ const single = unwrapSingle(byIdResponse.result);
+ if (single) return single;
+ }
+ const users = await getUsers();
+ return (
+ users.find((user) => String(user.UserID) === String(userId)) || null
);
},
[endpoints.users, getUsers],
@@ -364,17 +280,7 @@ const useGlobalHook = () => {
if (!isGlobalApiReady()) return null;
const response = await API.post(withKey(endpoints.users), payload);
if (!response.isSuccess) return null;
- const created = unwrapSingle(response.result);
- const createdUserId = created?.UserID ?? payload?.UserID;
- if (createdUserId !== undefined && createdUserId !== null) {
- writeGlobalDataCache(
- CACHE_KEY.userById(createdUserId),
- created || payload,
- CACHE_TTL.users,
- );
- }
- clearGlobalDataCache(CACHE_KEY.users);
- return created;
+ return unwrapSingle(response.result);
},
[endpoints.users],
);
@@ -387,14 +293,7 @@ const useGlobalHook = () => {
payload,
);
if (!response.isSuccess) return null;
- const updated = unwrapSingle(response.result) || payload;
- writeGlobalDataCache(
- CACHE_KEY.userById(userId),
- updated,
- CACHE_TTL.users,
- );
- clearGlobalDataCache(CACHE_KEY.users);
- return updated;
+ return unwrapSingle(response.result) || payload;
},
[endpoints.users],
);
@@ -420,7 +319,7 @@ const useGlobalHook = () => {
if (created) return created;
// Final fallback: check list endpoint again after attempted creation.
- const refreshed = await getUsers({ forceRefresh: true });
+ const refreshed = await getUsers();
return (
refreshed.find(
(user) => String(user.UserUsername) === String(seed.UserUsername),
@@ -446,6 +345,7 @@ const useGlobalHook = () => {
getFindsByEvent,
getFindsByPlayer,
logFind,
+ claimCache,
getUser,
createUser,
updateUser,