fixed claim cache for global mode

removed redundant code. fixed cache claiming logic with find item endopoint from geoquest api
This commit is contained in:
suddenlyoranges committed 2026-04-09 23:45:26 +01:00
1 parent c68eb160cc
commit 82e15508c2
12 files changed
+1182 -667

No files matched your search

+12 -13
View File
@@ -1,11 +1,11 @@
import {StyleSheet, TextInput, View} from 'react-native'; import { StyleSheet, TextInput, View } from "react-native";
import {Button} from './Button'; import { Button } from "./Button";
const CodeJoinRow = ({ const CodeJoinRow = ({
value, value,
onChange, onChange,
placeholder, placeholder,
buttonLabel = 'Join', buttonLabel = "Join",
onSubmit, onSubmit,
disabled = false, disabled = false,
style, style,
@@ -39,37 +39,36 @@ const CodeJoinRow = ({
const styles = StyleSheet.create({ const styles = StyleSheet.create({
inputRow: { inputRow: {
flexDirection: 'row', flexDirection: "row",
gap: 10, gap: 10,
marginBottom: 15, marginBottom: 15,
width: '100%', width: "100%",
paddingHorizontal: 20, paddingHorizontal: 20,
}, },
codeInput: { codeInput: {
flex: 1, flex: 1,
borderWidth: 1, borderWidth: 1,
borderColor: '#45475a', borderColor: "#45475a",
borderRadius: 8, borderRadius: 8,
paddingHorizontal: 12, paddingHorizontal: 12,
paddingVertical: 10, paddingVertical: 10,
fontSize: 16, fontSize: 16,
color: '#cdd6f4', color: "#cdd6f4",
backgroundColor: '#313244', backgroundColor: "#313244",
}, },
inputDisabled: { inputDisabled: {
opacity: 0.5, opacity: 0.5,
}, },
joinButton: { joinButton: {
backgroundColor: '#bd93f9', backgroundColor: "#bd93f9",
borderColor: '#bd93f9', borderColor: "#bd93f9",
flex: 0, flex: 0,
paddingHorizontal: 20, paddingHorizontal: 20,
}, },
joinLabel: { joinLabel: {
color: '#1e1e2e', color: "#1e1e2e",
fontWeight: '600', fontWeight: "600",
}, },
}); });
export default CodeJoinRow; export default CodeJoinRow;
+12 -12
View File
@@ -1,6 +1,6 @@
import React, {useEffect, useRef, useState} from 'react'; import { useEffect, useRef, useState } from "react";
import {StyleSheet, Text, View} from 'react-native'; import { StyleSheet, Text, View } from "react-native";
import {CACHE_CLAIM_TIMER} from '../../utils/geoMath'; import { CACHE_CLAIM_TIMER } from "../../utils/geoMath";
const ClaimTimerView = ({ const ClaimTimerView = ({
cache, cache,
@@ -8,7 +8,7 @@ const ClaimTimerView = ({
isClaiming, isClaiming,
showClaimedPopup, showClaimedPopup,
claimDurationSeconds = CACHE_CLAIM_TIMER, claimDurationSeconds = CACHE_CLAIM_TIMER,
claimedPopupMessage = 'Cache Claimed!', claimedPopupMessage = "Cache Claimed!",
autoClaim = true, autoClaim = true,
}) => { }) => {
const [timeLeft, setTimeLeft] = useState(claimDurationSeconds); const [timeLeft, setTimeLeft] = useState(claimDurationSeconds);
@@ -75,30 +75,30 @@ const ClaimTimerView = ({
const styles = StyleSheet.create({ const styles = StyleSheet.create({
overlay: { overlay: {
alignSelf: 'center', alignSelf: "center",
backgroundColor: 'rgba(0, 0, 0, 0.75)', backgroundColor: "rgba(0, 0, 0, 0.75)",
paddingHorizontal: 16, paddingHorizontal: 16,
paddingVertical: 12, paddingVertical: 12,
borderRadius: 10, borderRadius: 10,
marginTop: 8, marginTop: 8,
}, },
text: { text: {
color: '#cdd6f4', color: "#cdd6f4",
fontSize: 16, fontSize: 16,
fontWeight: '700', fontWeight: "700",
}, },
claimedOverlay: { claimedOverlay: {
alignSelf: 'center', alignSelf: "center",
backgroundColor: '#a6e3a1', backgroundColor: "#a6e3a1",
paddingHorizontal: 20, paddingHorizontal: 20,
paddingVertical: 14, paddingVertical: 14,
borderRadius: 10, borderRadius: 10,
marginTop: 8, marginTop: 8,
}, },
claimedText: { claimedText: {
color: '#1e1e2e', color: "#1e1e2e",
fontSize: 18, fontSize: 18,
fontWeight: '700', fontWeight: "700",
}, },
}); });
+9 -7
View File
@@ -1,11 +1,11 @@
import React from 'react'; import MapView, { Marker, Polygon } from "react-native-maps";
import MapView, {Marker, Polygon} from 'react-native-maps'; import { getFovCone } from "../../utils/geoMath";
import {getFovCone} from '../../utils/geoMath';
const PlayerMapView = ({ userLocation, visibleCaches, heading }) => { const PlayerMapView = ({ userLocation, visibleCaches, heading }) => {
if (!userLocation) return null; if (!userLocation) return null;
const coneCoords = (heading !== null && heading !== undefined) const coneCoords =
heading !== null && heading !== undefined
? getFovCone(userLocation, heading) ? getFovCone(userLocation, heading)
: null; : null;
@@ -13,7 +13,11 @@ const PlayerMapView = ({userLocation, visibleCaches, heading}) => {
<MapView <MapView
style={{ flex: 1 }} style={{ flex: 1 }}
provider="google" provider="google"
initialRegion={{...userLocation, latitudeDelta: 0.01, longitudeDelta: 0.01}} initialRegion={{
...userLocation,
latitudeDelta: 0.01,
longitudeDelta: 0.01,
}}
scrollEnabled={true} scrollEnabled={true}
zoomEnabled={true} zoomEnabled={true}
rotateEnabled={false} rotateEnabled={false}
@@ -41,5 +45,3 @@ const PlayerMapView = ({userLocation, visibleCaches, heading}) => {
}; };
export default PlayerMapView; export default PlayerMapView;
@@ -1,8 +1,17 @@
import React, {useEffect, useState} from 'react'; import { useEffect, useState } from "react";
import {ActivityIndicator, Alert, ScrollView, StyleSheet, Switch, Text, TextInput, View} from 'react-native'; import {
import Screen from '../layout/Screen'; ActivityIndicator,
import {Button, ButtonTray} from '../UI/Button'; Alert,
import useGameHook from '../../hooks/useGameHook'; 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 DepartmentSettingsScreen = ({ route, navigation }) => {
const { SGid, Gid } = route.params; const { SGid, Gid } = route.params;
@@ -10,10 +19,10 @@ const DepartmentSettingsScreen = ({route, navigation}) => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [deptName, setDeptName] = useState(''); const [deptName, setDeptName] = useState("");
const [joinCode, setJoinCode] = useState(''); const [joinCode, setJoinCode] = useState("");
const [teamsEnabled, setTeamsEnabled] = useState(false); const [teamsEnabled, setTeamsEnabled] = useState(false);
const [cacheTriggerMeters, setCacheTriggerMeters] = useState('20'); const [cacheTriggerMeters, setCacheTriggerMeters] = useState("20");
const [cacheCount, setCacheCount] = useState(0); const [cacheCount, setCacheCount] = useState(0);
// Handlers ------------------- // Handlers -------------------
@@ -22,8 +31,8 @@ const DepartmentSettingsScreen = ({route, navigation}) => {
const load = async () => { const load = async () => {
const sg = await getSubgroup(SGid); const sg = await getSubgroup(SGid);
if (sg) { if (sg) {
setDeptName(sg.SubGroupName || ''); setDeptName(sg.SubGroupName || "");
setJoinCode(sg.JoinCode || ''); setJoinCode(sg.JoinCode || "");
setTeamsEnabled(Boolean(sg.TeamsEnabled)); setTeamsEnabled(Boolean(sg.TeamsEnabled));
setCacheTriggerMeters(String(sg.CacheTriggerMeters || 20)); setCacheTriggerMeters(String(sg.CacheTriggerMeters || 20));
} }
@@ -43,21 +52,21 @@ const DepartmentSettingsScreen = ({route, navigation}) => {
CacheTriggerMeters: parseInt(cacheTriggerMeters) || 20, CacheTriggerMeters: parseInt(cacheTriggerMeters) || 20,
}); });
setSaving(false); setSaving(false);
Alert.alert('Saved', 'Department settings updated.'); Alert.alert("Saved", "Department settings updated.");
}; };
const handleResetDepartment = () => { const handleResetDepartment = () => {
Alert.alert( Alert.alert(
'Reset Department', "Reset Department",
'This will reset all cache claims for this department. Are you sure?', "This will reset all cache claims for this department. Are you sure?",
[ [
{text: 'Cancel', style: 'cancel'}, { text: "Cancel", style: "cancel" },
{ {
text: 'Reset', text: "Reset",
style: 'destructive', style: "destructive",
onPress: async () => { onPress: async () => {
await resetGame(Gid, SGid); await resetGame(Gid, SGid);
Alert.alert('Done', 'Department progress has been reset.'); Alert.alert("Done", "Department progress has been reset.");
}, },
}, },
], ],
@@ -65,7 +74,7 @@ const DepartmentSettingsScreen = ({route, navigation}) => {
}; };
const handleManageCaches = () => { const handleManageCaches = () => {
navigation.navigate('MapScreen', { navigation.navigate("MapScreen", {
selectedDepartmentSGid: SGid, selectedDepartmentSGid: SGid,
selectedDepartmentName: deptName || null, selectedDepartmentName: deptName || null,
}); });
@@ -83,7 +92,10 @@ const DepartmentSettingsScreen = ({route, navigation}) => {
return ( return (
<Screen showBack={true}> <Screen showBack={true}>
<ScrollView contentContainerStyle={styles.formContainer} showsVerticalScrollIndicator={false}> <ScrollView
contentContainerStyle={styles.formContainer}
showsVerticalScrollIndicator={false}
>
<Text style={styles.sectionTitle}>Department Settings</Text> <Text style={styles.sectionTitle}>Department Settings</Text>
<Text style={styles.label}>Department Name</Text> <Text style={styles.label}>Department Name</Text>
@@ -97,7 +109,7 @@ const DepartmentSettingsScreen = ({route, navigation}) => {
<View style={styles.codeSection}> <View style={styles.codeSection}>
<Text style={styles.codeLabel}>Department Join Code</Text> <Text style={styles.codeLabel}>Department Join Code</Text>
<Text style={styles.codeValue}>{joinCode || '—'}</Text> <Text style={styles.codeValue}>{joinCode || "—"}</Text>
</View> </View>
<View style={styles.toggleRow}> <View style={styles.toggleRow}>
@@ -105,8 +117,8 @@ const DepartmentSettingsScreen = ({route, navigation}) => {
<Switch <Switch
value={teamsEnabled} value={teamsEnabled}
onValueChange={setTeamsEnabled} onValueChange={setTeamsEnabled}
trackColor={{false: '#d1d5db', true: '#93c5fd'}} trackColor={{ false: "#d1d5db", true: "#93c5fd" }}
thumbColor={teamsEnabled ? '#2563eb' : '#9ca3af'} thumbColor={teamsEnabled ? "#2563eb" : "#9ca3af"}
/> />
</View> </View>
@@ -128,7 +140,7 @@ const DepartmentSettingsScreen = ({route, navigation}) => {
<View style={styles.saveWrap}> <View style={styles.saveWrap}>
<ButtonTray> <ButtonTray>
<Button <Button
label={saving ? 'Saving...' : 'Save Settings'} label={saving ? "Saving..." : "Save Settings"}
onClick={handleSave} onClick={handleSave}
styleButton={styles.saveButton} styleButton={styles.saveButton}
styleLabel={styles.saveLabel} styleLabel={styles.saveLabel}
@@ -161,64 +173,79 @@ const DepartmentSettingsScreen = ({route, navigation}) => {
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
center: {justifyContent: 'center', alignItems: 'center'}, center: { justifyContent: "center", alignItems: "center" },
formContainer: { paddingBottom: 20 }, formContainer: { paddingBottom: 20 },
sectionTitle: {fontSize: 20, fontWeight: '700', color: '#cdd6f4', marginBottom: 16}, sectionTitle: {
label: {fontSize: 14, fontWeight: '600', color: '#bac2de', marginBottom: 6, marginTop: 14}, fontSize: 20,
fontWeight: "700",
color: "#cdd6f4",
marginBottom: 16,
},
label: {
fontSize: 14,
fontWeight: "600",
color: "#bac2de",
marginBottom: 6,
marginTop: 14,
},
input: { input: {
borderWidth: 1, borderWidth: 1,
borderColor: '#45475a', borderColor: "#45475a",
borderRadius: 8, borderRadius: 8,
paddingHorizontal: 12, paddingHorizontal: 12,
paddingVertical: 10, paddingVertical: 10,
fontSize: 16, fontSize: 16,
color: '#cdd6f4', color: "#cdd6f4",
backgroundColor: '#313244', backgroundColor: "#313244",
}, },
codeSection: { codeSection: {
flexDirection: 'row', flexDirection: "row",
justifyContent: 'space-between', justifyContent: "space-between",
alignItems: 'center', alignItems: "center",
marginTop: 16, marginTop: 16,
paddingVertical: 12, paddingVertical: 12,
paddingHorizontal: 12, paddingHorizontal: 12,
backgroundColor: '#313244', backgroundColor: "#313244",
borderRadius: 8, borderRadius: 8,
}, },
codeLabel: {fontSize: 14, fontWeight: '600', color: '#bac2de'}, codeLabel: { fontSize: 14, fontWeight: "600", color: "#bac2de" },
codeValue: {fontSize: 16, fontWeight: '700', color: '#bd93f9', letterSpacing: 2}, codeValue: {
fontSize: 16,
fontWeight: "700",
color: "#bd93f9",
letterSpacing: 2,
},
toggleRow: { toggleRow: {
flexDirection: 'row', flexDirection: "row",
justifyContent: 'space-between', justifyContent: "space-between",
alignItems: 'center', alignItems: "center",
marginTop: 16, marginTop: 16,
paddingVertical: 10, paddingVertical: 10,
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: '#45475a', borderBottomColor: "#45475a",
}, },
toggleLabel: {fontSize: 14, fontWeight: '600', color: '#bac2de'}, toggleLabel: { fontSize: 14, fontWeight: "600", color: "#bac2de" },
infoRow: { infoRow: {
flexDirection: 'row', flexDirection: "row",
justifyContent: 'space-between', justifyContent: "space-between",
alignItems: 'center', alignItems: "center",
marginTop: 16, marginTop: 16,
paddingVertical: 12, paddingVertical: 12,
paddingHorizontal: 12, paddingHorizontal: 12,
backgroundColor: '#313244', backgroundColor: "#313244",
borderRadius: 8, borderRadius: 8,
}, },
infoLabel: {fontSize: 14, fontWeight: '600', color: '#bac2de'}, infoLabel: { fontSize: 14, fontWeight: "600", color: "#bac2de" },
infoValue: {fontSize: 16, fontWeight: '700', color: '#cdd6f4'}, infoValue: { fontSize: 16, fontWeight: "700", color: "#cdd6f4" },
saveWrap: { marginTop: 24 }, saveWrap: { marginTop: 24 },
saveButton: {backgroundColor: '#a6e3a1', borderColor: '#a6e3a1'}, saveButton: { backgroundColor: "#a6e3a1", borderColor: "#a6e3a1" },
saveLabel: {color: '#1e1e2e', fontWeight: '600'}, saveLabel: { color: "#1e1e2e", fontWeight: "600" },
mapWrap: { marginTop: 16 }, mapWrap: { marginTop: 16 },
mapButton: {backgroundColor: '#bd93f9', borderColor: '#bd93f9'}, mapButton: { backgroundColor: "#bd93f9", borderColor: "#bd93f9" },
mapLabel: {color: '#1e1e2e', fontWeight: '600'}, mapLabel: { color: "#1e1e2e", fontWeight: "600" },
resetWrap: { marginTop: 30 }, resetWrap: { marginTop: 30 },
resetButton: {backgroundColor: '#86efac', borderColor: '#86efac'}, resetButton: { backgroundColor: "#86efac", borderColor: "#86efac" },
resetLabel: {color: '#1e1e2e', fontWeight: '600'}, resetLabel: { color: "#1e1e2e", fontWeight: "600" },
}); });
export default DepartmentSettingsScreen; export default DepartmentSettingsScreen;
+225 -101
View File
@@ -1,15 +1,36 @@
import React, {useCallback, useEffect, useRef, useState} from 'react'; import { useCallback, useEffect, useRef, useState } from "react";
import {ActivityIndicator, Alert, ScrollView, StyleSheet, Switch, Text, TextInput, View} from 'react-native'; import {
import {useFocusEffect} from '@react-navigation/native'; ActivityIndicator,
import Screen from '../layout/Screen'; Alert,
import Card from '../UI/Card'; ScrollView,
import {Button, ButtonTray} from '../UI/Button'; StyleSheet,
import useGameHook from '../../hooks/useGameHook'; Switch,
import {getSession, setSessionTeamsEnabled} from '../../hooks/SessionStore'; 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 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({ const gameApiRef = useRef({
getLobby, getLobby,
getSubgroups, getSubgroups,
@@ -28,25 +49,32 @@ const GameSettingsScreen = ({navigation}) => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [groupName, setGroupName] = useState(''); const [groupName, setGroupName] = useState("");
const [businessName, setBusinessName] = useState(''); const [businessName, setBusinessName] = useState("");
const [teamsEnabled, setTeamsEnabled] = useState(session.teamsEnabled); const [teamsEnabled, setTeamsEnabled] = useState(session.teamsEnabled);
const [serverTeamsEnabled, setServerTeamsEnabled] = useState(session.teamsEnabled); const [serverTeamsEnabled, setServerTeamsEnabled] = useState(
const [cacheTriggerMeters, setCacheTriggerMeters] = useState('20'); session.teamsEnabled,
const [adminJoinCode, setAdminJoinCode] = useState(''); );
const [orgJoinCode, setOrgJoinCode] = useState(''); const [cacheTriggerMeters, setCacheTriggerMeters] = useState("20");
const [memberJoinCode, setMemberJoinCode] = useState(''); const [adminJoinCode, setAdminJoinCode] = useState("");
const [orgJoinCode, setOrgJoinCode] = useState("");
const [memberJoinCode, setMemberJoinCode] = useState("");
const [waitlist, setWaitlist] = useState([]); const [waitlist, setWaitlist] = useState([]);
const [waitlistNames, setWaitlistNames] = useState({}); const [waitlistNames, setWaitlistNames] = useState({});
const [isBusinessGroup, setIsBusinessGroup] = useState(Boolean(session.isBusiness)); const [isBusinessGroup, setIsBusinessGroup] = useState(
Boolean(session.isBusiness),
);
const [subgroupList, setSubgroupList] = useState([]); const [subgroupList, setSubgroupList] = useState([]);
const [newDeptName, setNewDeptName] = useState(''); const [newDeptName, setNewDeptName] = useState("");
// Handlers ------------------- // Handlers -------------------
useEffect(() => () => { useEffect(
() => () => {
isMountedRef.current = false; isMountedRef.current = false;
}, []); },
[],
);
const refreshSettings = useCallback(async ({ showLoader = false } = {}) => { const refreshSettings = useCallback(async ({ showLoader = false } = {}) => {
const activeSession = getSession(); const activeSession = getSession();
@@ -71,8 +99,8 @@ const GameSettingsScreen = ({navigation}) => {
if (!isMountedRef.current) return; if (!isMountedRef.current) return;
if (group) { if (group) {
setGroupName(group.GroupName || ''); setGroupName(group.GroupName || "");
setBusinessName(group.BusinessOrSchoolName || ''); setBusinessName(group.BusinessOrSchoolName || "");
// Organisation mode // Organisation mode
setIsBusinessGroup(Boolean(group.BusinessOrSchoolName)); setIsBusinessGroup(Boolean(group.BusinessOrSchoolName));
@@ -82,17 +110,17 @@ const GameSettingsScreen = ({navigation}) => {
setSessionTeamsEnabled(serverTeams); setSessionTeamsEnabled(serverTeams);
setCacheTriggerMeters(String(group.CacheTriggerMeters || 20)); setCacheTriggerMeters(String(group.CacheTriggerMeters || 20));
setAdminJoinCode(group.AdminJoinCode || ''); setAdminJoinCode(group.AdminJoinCode || "");
setOrgJoinCode(group.OrgJoinCode || ''); setOrgJoinCode(group.OrgJoinCode || "");
} }
setSubgroupList(sgs || []); setSubgroupList(sgs || []);
const memberSg = (sgs || []).find((sg) => !sg.IsAdminGroup); const memberSg = (sgs || []).find((sg) => !sg.IsAdminGroup);
setMemberJoinCode(memberSg?.JoinCode || ''); setMemberJoinCode(memberSg?.JoinCode || "");
setWaitlist(waitlistRows || []); setWaitlist(waitlistRows || []);
const names = {}; const names = {};
for (const entry of (waitlistRows || [])) { for (const entry of waitlistRows || []) {
const user = await gameApiRef.current.getUser(entry.Uid); const user = await gameApiRef.current.getUser(entry.Uid);
if (user) names[entry.Uid] = user.username; if (user) names[entry.Uid] = user.username;
} }
@@ -146,11 +174,11 @@ const GameSettingsScreen = ({navigation}) => {
// Individual save — check teams toggle // Individual save — check teams toggle
if (serverTeamsEnabled && !teamsEnabled) { if (serverTeamsEnabled && !teamsEnabled) {
Alert.alert( Alert.alert(
'Disable Teams', "Disable Teams",
'This will disband all existing teams and remove all players from their teams. Continue?', "This will disband all existing teams and remove all players from their teams. Continue?",
[ [
{text: 'Cancel', style: 'cancel'}, { text: "Cancel", style: "cancel" },
{text: 'Disable', style: 'destructive', onPress: saveSettings}, { text: "Disable", style: "destructive", onPress: saveSettings },
], ],
); );
return; return;
@@ -159,17 +187,24 @@ const GameSettingsScreen = ({navigation}) => {
}; };
const handleResetGame = () => { const handleResetGame = () => {
Alert.alert('Reset Entire Game', 'This will remove ALL saved caches and reset progress for every player. Are you sure?', [ Alert.alert(
{text: 'Cancel', style: 'cancel'}, "Reset Entire Game",
"This will remove ALL saved caches and reset progress for every player. Are you sure?",
[
{ text: "Cancel", style: "cancel" },
{ {
text: 'Reset', text: "Reset",
style: 'destructive', style: "destructive",
onPress: async () => { onPress: async () => {
await resetGame(session.currentGid); await resetGame(session.currentGid);
Alert.alert('Done', 'All caches and player progress have been reset.'); Alert.alert(
"Done",
"All caches and player progress have been reset.",
);
}, },
}, },
]); ],
);
}; };
const handleApproveAdmin = async (entry) => { const handleApproveAdmin = async (entry) => {
@@ -193,28 +228,32 @@ const GameSettingsScreen = ({navigation}) => {
IsAdminGroup: false, IsAdminGroup: false,
CacheTriggerMeters: 20, CacheTriggerMeters: 20,
}); });
setNewDeptName(''); setNewDeptName("");
await refreshSettings(); await refreshSettings();
}; };
// Business — delete a department // Business — delete a department
const handleDeleteDepartment = (sg) => { const handleDeleteDepartment = (sg) => {
Alert.alert('Delete Department', `Delete "${sg.SubGroupName}" and all its data?`, [ Alert.alert(
{text: 'Cancel', style: 'cancel'}, "Delete Department",
`Delete "${sg.SubGroupName}" and all its data?`,
[
{ text: "Cancel", style: "cancel" },
{ {
text: 'Delete', text: "Delete",
style: 'destructive', style: "destructive",
onPress: async () => { onPress: async () => {
await deleteSubgroup(sg.SGid); await deleteSubgroup(sg.SGid);
await refreshSettings(); await refreshSettings();
}, },
}, },
]); ],
);
}; };
// Business — navigate to department settings // Business — navigate to department settings
const handleEnterDepartment = (sg) => { const handleEnterDepartment = (sg) => {
navigation.navigate('DepartmentSettingsScreen', { navigation.navigate("DepartmentSettingsScreen", {
SGid: sg.SGid, SGid: sg.SGid,
Gid: session.currentGid, Gid: session.currentGid,
SubGroupName: sg.SubGroupName, SubGroupName: sg.SubGroupName,
@@ -246,7 +285,10 @@ const GameSettingsScreen = ({navigation}) => {
if (isBusinessGroup) { if (isBusinessGroup) {
return ( return (
<Screen> <Screen>
<ScrollView contentContainerStyle={styles.formContainer} showsVerticalScrollIndicator={false}> <ScrollView
contentContainerStyle={styles.formContainer}
showsVerticalScrollIndicator={false}
>
<Text style={styles.sectionTitle}>Game Configuration :</Text> <Text style={styles.sectionTitle}>Game Configuration :</Text>
<Text style={styles.label}>Organisation Name:</Text> <Text style={styles.label}>Organisation Name:</Text>
@@ -260,18 +302,18 @@ const GameSettingsScreen = ({navigation}) => {
<View style={styles.codeSection}> <View style={styles.codeSection}>
<Text style={styles.codeLabel}>Admin join code:</Text> <Text style={styles.codeLabel}>Admin join code:</Text>
<Text style={styles.codeValue}>{adminJoinCode || '—'}</Text> <Text style={styles.codeValue}>{adminJoinCode || "—"}</Text>
</View> </View>
<View style={styles.codeSection}> <View style={styles.codeSection}>
<Text style={styles.codeLabel}>Org join code:</Text> <Text style={styles.codeLabel}>Org join code:</Text>
<Text style={styles.codeValue}>{orgJoinCode || '—'}</Text> <Text style={styles.codeValue}>{orgJoinCode || "—"}</Text>
</View> </View>
<View style={styles.saveWrap}> <View style={styles.saveWrap}>
<ButtonTray> <ButtonTray>
<Button <Button
label={saving ? 'Saving...' : 'Save Settings'} label={saving ? "Saving..." : "Save Settings"}
onClick={handleSave} onClick={handleSave}
styleButton={styles.saveButton} styleButton={styles.saveButton}
styleLabel={styles.saveLabel} styleLabel={styles.saveLabel}
@@ -284,7 +326,9 @@ const GameSettingsScreen = ({navigation}) => {
<Text style={styles.sectionTitle}>Departments:</Text> <Text style={styles.sectionTitle}>Departments:</Text>
{departments.map((sg, index) => ( {departments.map((sg, index) => (
<Card key={sg.SGid}> <Card key={sg.SGid}>
<Text style={styles.deptIndexLabel}>Department {index + 1}:</Text> <Text style={styles.deptIndexLabel}>
Department {index + 1}:
</Text>
<View style={styles.deptCardRow}> <View style={styles.deptCardRow}>
<View style={styles.deptCardInfo}> <View style={styles.deptCardInfo}>
<Text style={styles.deptInfoLabel}>Department Name</Text> <Text style={styles.deptInfoLabel}>Department Name</Text>
@@ -301,8 +345,10 @@ const GameSettingsScreen = ({navigation}) => {
</View> </View>
<View style={styles.deptCardRow}> <View style={styles.deptCardRow}>
<View style={styles.deptCardInfo}> <View style={styles.deptCardInfo}>
<Text style={styles.deptInfoLabel}>Department Join code</Text> <Text style={styles.deptInfoLabel}>
<Text style={styles.deptCode}>{sg.JoinCode || '—'}</Text> Department Join code
</Text>
<Text style={styles.deptCode}>{sg.JoinCode || "—"}</Text>
</View> </View>
<View style={styles.deptCardActions}> <View style={styles.deptCardActions}>
<Button <Button
@@ -382,7 +428,10 @@ const GameSettingsScreen = ({navigation}) => {
// ===== Individual Admin View ===== // ===== Individual Admin View =====
return ( return (
<Screen> <Screen>
<ScrollView contentContainerStyle={styles.formContainer} showsVerticalScrollIndicator={false}> <ScrollView
contentContainerStyle={styles.formContainer}
showsVerticalScrollIndicator={false}
>
<Text style={styles.sectionTitle}>Game Configuration</Text> <Text style={styles.sectionTitle}>Game Configuration</Text>
<Text style={styles.label}>Game Name</Text> <Text style={styles.label}>Game Name</Text>
@@ -399,8 +448,8 @@ const GameSettingsScreen = ({navigation}) => {
<Switch <Switch
value={teamsEnabled} value={teamsEnabled}
onValueChange={setTeamsEnabled} onValueChange={setTeamsEnabled}
trackColor={{false: '#d1d5db', true: '#93c5fd'}} trackColor={{ false: "#d1d5db", true: "#93c5fd" }}
thumbColor={teamsEnabled ? '#2563eb' : '#9ca3af'} thumbColor={teamsEnabled ? "#2563eb" : "#9ca3af"}
/> />
</View> </View>
@@ -416,18 +465,18 @@ const GameSettingsScreen = ({navigation}) => {
<View style={styles.codeSection}> <View style={styles.codeSection}>
<Text style={styles.codeLabel}>Admin Join Code</Text> <Text style={styles.codeLabel}>Admin Join Code</Text>
<Text style={styles.codeValue}>{adminJoinCode || '—'}</Text> <Text style={styles.codeValue}>{adminJoinCode || "—"}</Text>
</View> </View>
<View style={styles.codeSection}> <View style={styles.codeSection}>
<Text style={styles.codeLabel}>Game Join Code</Text> <Text style={styles.codeLabel}>Game Join Code</Text>
<Text style={styles.codeValue}>{memberJoinCode || '—'}</Text> <Text style={styles.codeValue}>{memberJoinCode || "—"}</Text>
</View> </View>
<View style={styles.saveWrap}> <View style={styles.saveWrap}>
<ButtonTray> <ButtonTray>
<Button <Button
label={saving ? 'Saving...' : 'Save Settings'} label={saving ? "Saving..." : "Save Settings"}
onClick={handleSave} onClick={handleSave}
styleButton={styles.saveButton} styleButton={styles.saveButton}
styleLabel={styles.saveLabel} styleLabel={styles.saveLabel}
@@ -480,77 +529,152 @@ const GameSettingsScreen = ({navigation}) => {
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
center: {justifyContent: 'center', alignItems: 'center'}, center: { justifyContent: "center", alignItems: "center" },
formContainer: { paddingBottom: 20 }, formContainer: { paddingBottom: 20 },
sectionTitle: {fontSize: 20, fontWeight: '700', color: '#cdd6f4', marginBottom: 16}, sectionTitle: {
label: {fontSize: 14, fontWeight: '600', color: '#bac2de', marginBottom: 6, marginTop: 14}, fontSize: 20,
body: {color: '#bac2de', fontSize: 15}, fontWeight: "700",
color: "#cdd6f4",
marginBottom: 16,
},
label: {
fontSize: 14,
fontWeight: "600",
color: "#bac2de",
marginBottom: 6,
marginTop: 14,
},
body: { color: "#bac2de", fontSize: 15 },
input: { input: {
borderWidth: 1, borderWidth: 1,
borderColor: '#45475a', borderColor: "#45475a",
borderRadius: 8, borderRadius: 8,
paddingHorizontal: 12, paddingHorizontal: 12,
paddingVertical: 10, paddingVertical: 10,
fontSize: 16, fontSize: 16,
color: '#cdd6f4', color: "#cdd6f4",
backgroundColor: '#313244', backgroundColor: "#313244",
}, },
toggleRow: { toggleRow: {
flexDirection: 'row', flexDirection: "row",
justifyContent: 'space-between', justifyContent: "space-between",
alignItems: 'center', alignItems: "center",
marginTop: 16, marginTop: 16,
paddingVertical: 10, paddingVertical: 10,
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: '#45475a', borderBottomColor: "#45475a",
}, },
toggleLabel: {fontSize: 14, fontWeight: '600', color: '#bac2de'}, toggleLabel: { fontSize: 14, fontWeight: "600", color: "#bac2de" },
codeSection: { codeSection: {
flexDirection: 'row', flexDirection: "row",
justifyContent: 'space-between', justifyContent: "space-between",
alignItems: 'center', alignItems: "center",
marginTop: 16, marginTop: 16,
paddingVertical: 12, paddingVertical: 12,
paddingHorizontal: 12, paddingHorizontal: 12,
backgroundColor: '#313244', backgroundColor: "#313244",
borderRadius: 8, borderRadius: 8,
}, },
codeLabel: {fontSize: 14, fontWeight: '600', color: '#bac2de'}, codeLabel: { fontSize: 14, fontWeight: "600", color: "#bac2de" },
codeValue: {fontSize: 16, fontWeight: '700', color: '#bd93f9', letterSpacing: 2}, codeValue: {
fontSize: 16,
fontWeight: "700",
color: "#bd93f9",
letterSpacing: 2,
},
saveWrap: { marginTop: 24 }, saveWrap: { marginTop: 24 },
saveButton: {backgroundColor: '#a6e3a1', borderColor: '#a6e3a1'}, saveButton: { backgroundColor: "#a6e3a1", borderColor: "#a6e3a1" },
saveLabel: {color: '#1e1e2e', fontWeight: '600'}, saveLabel: { color: "#1e1e2e", fontWeight: "600" },
deptSection: { marginTop: 30 }, deptSection: { marginTop: 30 },
deptIndexLabel: {fontSize: 14, fontWeight: '700', color: '#bac2de', marginBottom: 8}, deptIndexLabel: {
deptCardRow: {flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8}, fontSize: 14,
fontWeight: "700",
color: "#bac2de",
marginBottom: 8,
},
deptCardRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 8,
},
deptCardInfo: { flex: 1, paddingRight: 10 }, deptCardInfo: { flex: 1, paddingRight: 10 },
deptCardActions: {flexDirection: 'row', gap: 6, flex: 0}, deptCardActions: { flexDirection: "row", gap: 6, flex: 0 },
deptInfoLabel: {fontSize: 12, fontWeight: '600', color: '#6c7086', marginBottom: 2}, deptInfoLabel: {
deptName: {fontSize: 15, fontWeight: '700', color: '#cdd6f4'}, fontSize: 12,
deptCode: {fontSize: 13, fontWeight: '600', color: '#bd93f9', marginTop: 2, letterSpacing: 1}, fontWeight: "600",
deptDeleteButton: {backgroundColor: '#D92800', borderColor: '#D92800', minHeight: 36, flex: 0, paddingHorizontal: 10}, color: "#6c7086",
deptOpenButton: {backgroundColor: '#bd93f9', borderColor: '#bd93f9', minHeight: 36, flex: 0, paddingHorizontal: 10}, marginBottom: 2,
deptBtnLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 13}, },
createDeptRow: {flexDirection: 'row', gap: 10, marginTop: 12, alignItems: 'center'}, deptName: { fontSize: 15, fontWeight: "700", color: "#cdd6f4" },
createDeptButton: {backgroundColor: '#a6e3a1', borderColor: '#a6e3a1', flex: 0, paddingHorizontal: 16}, deptCode: {
createDeptLabel: {color: '#1e1e2e', fontWeight: '600'}, fontSize: 13,
emptyText: {color: '#6c7086', textAlign: 'center', marginTop: 10, fontSize: 14}, 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 }, waitlistSection: { marginTop: 30 },
waitlistRow: { waitlistRow: {
flexDirection: 'row', flexDirection: "row",
justifyContent: 'space-between', justifyContent: "space-between",
alignItems: 'center', alignItems: "center",
}, },
waitlistName: {fontSize: 15, fontWeight: '600', color: '#cdd6f4', flex: 1}, waitlistName: { fontSize: 15, fontWeight: "600", color: "#cdd6f4", flex: 1 },
waitlistActions: {flexDirection: 'row', gap: 6}, waitlistActions: { flexDirection: "row", gap: 6 },
approveButton: {backgroundColor: '#a6e3a1', borderColor: '#a6e3a1', minHeight: 36, flex: 0, paddingHorizontal: 10}, approveButton: {
approveBtnLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 13}, backgroundColor: "#a6e3a1",
rejectButton: {backgroundColor: '#D92800', borderColor: '#D92800', minHeight: 36, flex: 0, paddingHorizontal: 10}, borderColor: "#a6e3a1",
rejectBtnLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 13}, 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 }, resetSection: { marginTop: 30 },
resetButton: {backgroundColor: '#D92800', borderColor: '#D92800'}, resetButton: { backgroundColor: "#D92800", borderColor: "#D92800" },
resetLabel: {color: '#ffffff', fontWeight: '600'}, resetLabel: { color: "#ffffff", fontWeight: "600" },
}); });
export default GameSettingsScreen; export default GameSettingsScreen;
+291 -104
View File
@@ -1,11 +1,19 @@
import React, {useCallback, useEffect, useState} from 'react'; import { useCallback, useEffect, useState } from "react";
import {ActivityIndicator, Alert, Pressable, ScrollView, StyleSheet, Text, View} from 'react-native'; import {
import Screen from '../layout/Screen'; ActivityIndicator,
import Card from '../UI/Card'; Alert,
import {Button} from '../UI/Button'; Pressable,
import PendingApprovalView from '../UI/PendingApprovalView'; ScrollView,
import useGameHook from '../../hooks/useGameHook'; StyleSheet,
import {getSession} from '../../hooks/SessionStore'; 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 }) => { const LeaderboardScreen = ({ navigation, route }) => {
// Initialisation ------------ // Initialisation ------------
@@ -15,15 +23,31 @@ const LeaderboardScreen = ({navigation, route}) => {
const isBusiness = Boolean(session.isBusiness); const isBusiness = Boolean(session.isBusiness);
const selectedDepartment = route?.params?.department || null; const selectedDepartment = route?.params?.department || null;
const effectiveSGid = isBusiness const effectiveSGid = isBusiness
? ((isAdmin && selectedDepartment?.SGid) ? selectedDepartment.SGid : (isAdmin ? null : session.currentSGid)) ? isAdmin && selectedDepartment?.SGid
? selectedDepartment.SGid
: isAdmin
? null
: session.currentSGid
: null; : null;
const {getLobby, getSubgroup, getTeams, getTeamMembers, getGroupMembers, getCaches, getUser, deleteTeam, resetPlayerProgress, resetTeamProgress, getSubgroups} = useGameHook(); const {
getLobby,
getSubgroup,
getTeams,
getTeamMembers,
getGroupMembers,
getCaches,
getUser,
deleteTeam,
resetPlayerProgress,
resetTeamProgress,
getSubgroups,
} = useGameHook();
// State ---------------------- // State ----------------------
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [teamsEnabled, setTeamsEnabled] = useState(false); const [teamsEnabled, setTeamsEnabled] = useState(false);
const [activeTab, setActiveTab] = useState('game'); const [activeTab, setActiveTab] = useState("game");
const [teamRankings, setTeamRankings] = useState([]); const [teamRankings, setTeamRankings] = useState([]);
const [playerRankings, setPlayerRankings] = useState([]); const [playerRankings, setPlayerRankings] = useState([]);
const [myTeamRanking, setMyTeamRanking] = useState([]); const [myTeamRanking, setMyTeamRanking] = useState([]);
@@ -34,7 +58,10 @@ const LeaderboardScreen = ({navigation, route}) => {
// Handlers ------------------- // Handlers -------------------
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
if (!session.currentGid) { setLoading(false); return; } if (!session.currentGid) {
setLoading(false);
return;
}
const group = await getLobby(session.currentGid); const group = await getLobby(session.currentGid);
@@ -61,13 +88,13 @@ const LeaderboardScreen = ({navigation, route}) => {
const teams = await getTeams(session.currentGid, effectiveSGid ?? null); const teams = await getTeams(session.currentGid, effectiveSGid ?? null);
const teamData = []; const teamData = [];
for (const team of (teams || [])) { for (const team of teams || []) {
const members = await getTeamMembers(team.Tid); const members = await getTeamMembers(team.Tid);
const memberData = []; const memberData = [];
for (const m of (members || [])) { for (const m of members || []) {
const user = await getUser(m.Uid); const user = await getUser(m.Uid);
const count = (caches || []).filter((c) => const count = (caches || []).filter((c) =>
(c.Claims || []).some((cl) => cl.Uid === m.Uid) (c.Claims || []).some((cl) => cl.Uid === m.Uid),
).length; ).length;
memberData.push({ memberData.push({
uid: m.Uid, uid: m.Uid,
@@ -78,11 +105,11 @@ const LeaderboardScreen = ({navigation, route}) => {
} }
memberData.sort((a, b) => b.caches - a.caches); memberData.sort((a, b) => b.caches - a.caches);
const totalCaches = (caches || []).filter((c) => const totalCaches = (caches || []).filter((c) =>
(c.Claims || []).some((cl) => cl.Tid === team.Tid) (c.Claims || []).some((cl) => cl.Tid === team.Tid),
).length; ).length;
teamData.push({ teamData.push({
tid: team.Tid, tid: team.Tid,
name: team.TeamName || 'Unnamed Team', name: team.TeamName || "Unnamed Team",
code: team.JoinCode, code: team.JoinCode,
totalCaches, totalCaches,
members: memberData, members: memberData,
@@ -99,12 +126,17 @@ const LeaderboardScreen = ({navigation, route}) => {
} }
} else { } else {
// Build player rankings (no teams) // Build player rankings (no teams)
const scopedMembers = await getGroupMembers(session.currentGid, effectiveSGid ?? null); const scopedMembers = await getGroupMembers(
session.currentGid,
effectiveSGid ?? null,
);
const playerData = []; const playerData = [];
for (const m of (scopedMembers || []).filter((mem) => !mem.IsAcceptedAdmin)) { for (const m of (scopedMembers || []).filter(
(mem) => !mem.IsAcceptedAdmin,
)) {
const user = await getUser(m.Uid); const user = await getUser(m.Uid);
const count = (caches || []).filter((c) => const count = (caches || []).filter((c) =>
(c.Claims || []).some((cl) => cl.Uid === m.Uid) (c.Claims || []).some((cl) => cl.Uid === m.Uid),
).length; ).length;
playerData.push({ playerData.push({
uid: m.Uid, uid: m.Uid,
@@ -117,54 +149,88 @@ const LeaderboardScreen = ({navigation, route}) => {
} }
setLoading(false); setLoading(false);
}, [session.currentGid, session.currentTid, session.currentSGid, isBusiness, isAdmin, selectedDepartment, selectedDepartment?.SGid, effectiveSGid]); }, [
session.currentGid,
session.currentTid,
session.currentSGid,
isBusiness,
isAdmin,
selectedDepartment,
selectedDepartment?.SGid,
effectiveSGid,
]);
useEffect(() => { loadData(); }, [loadData]); useEffect(() => {
loadData();
}, [loadData]);
// Admin handler: delete a team and reload // Admin handler: delete a team and reload
const handleDeleteTeam = (team) => { const handleDeleteTeam = (team) => {
Alert.alert('Delete Team', `Delete "${team.name}" and kick all its members?`, [ Alert.alert(
{text: 'Cancel', style: 'cancel'}, "Delete Team",
`Delete "${team.name}" and kick all its members?`,
[
{ text: "Cancel", style: "cancel" },
{ {
text: 'Delete', text: "Delete",
style: 'destructive', style: "destructive",
onPress: async () => { onPress: async () => {
await deleteTeam(team.tid); await deleteTeam(team.tid);
await loadData(); await loadData();
}, },
}, },
]); ],
);
}; };
// Admin handler: reset a single player's progress // Admin handler: reset a single player's progress
const handleResetPlayer = (player) => { const handleResetPlayer = (player) => {
Alert.alert('Reset Progress', `Reset all cache claims for ${player.name}?`, [ Alert.alert(
{text: 'Cancel', style: 'cancel'}, "Reset Progress",
`Reset all cache claims for ${player.name}?`,
[
{ text: "Cancel", style: "cancel" },
{ {
text: 'Reset', text: "Reset",
style: 'destructive', style: "destructive",
onPress: async () => { onPress: async () => {
await resetPlayerProgress(session.currentGid, player.uid, effectiveSGid); await resetPlayerProgress(
Alert.alert('Done', `Progress for ${player.name} has been reset.`); session.currentGid,
player.uid,
effectiveSGid,
);
Alert.alert("Done", `Progress for ${player.name} has been reset.`);
await loadData(); await loadData();
}, },
}, },
]); ],
);
}; };
const handleResetTeam = (team) => { const handleResetTeam = (team) => {
Alert.alert('Reset Team Progress', `Reset all cache claims for team "${team.name}"?`, [ Alert.alert(
{text: 'Cancel', style: 'cancel'}, "Reset Team Progress",
`Reset all cache claims for team "${team.name}"?`,
[
{ text: "Cancel", style: "cancel" },
{ {
text: 'Reset', text: "Reset",
style: 'destructive', style: "destructive",
onPress: async () => { onPress: async () => {
await resetTeamProgress(session.currentGid, team.tid, effectiveSGid); await resetTeamProgress(
Alert.alert('Done', `Progress for team "${team.name}" has been reset.`); session.currentGid,
team.tid,
effectiveSGid,
);
Alert.alert(
"Done",
`Progress for team "${team.name}" has been reset.`,
);
await loadData(); await loadData();
}, },
}, },
]); ],
);
}; };
// View ----------------------- // View -----------------------
@@ -197,7 +263,9 @@ const LeaderboardScreen = ({navigation, route}) => {
{departmentList.map((department) => ( {departmentList.map((department) => (
<Pressable <Pressable
key={department.SGid} key={department.SGid}
onPress={() => navigation.navigate('LeaderboardScreen', {department})} onPress={() =>
navigation.navigate("LeaderboardScreen", { department })
}
> >
<Card> <Card>
<View style={styles.rankRow}> <View style={styles.rankRow}>
@@ -218,22 +286,39 @@ const LeaderboardScreen = ({navigation, route}) => {
// Helper: render an expandable team card (admin version with delete + reset) // Helper: render an expandable team card (admin version with delete + reset)
const renderAdminTeamCard = (team, index) => ( const renderAdminTeamCard = (team, index) => (
<Card key={team.tid}> <Card key={team.tid}>
<Pressable onPress={() => setExpandedTeam(expandedTeam === team.tid ? null : team.tid)}> <Pressable
onPress={() =>
setExpandedTeam(expandedTeam === team.tid ? null : team.tid)
}
>
<View style={styles.rankRow}> <View style={styles.rankRow}>
<Text style={styles.rank}>#{index + 1}</Text> <Text style={styles.rank}>#{index + 1}</Text>
<Text style={styles.rankName}>{team.name} ({team.code})</Text> <Text style={styles.rankName}>
<Text style={styles.score}>{team.totalCaches}/{totalCacheCount}</Text> {team.name} ({team.code})
</Text>
<Text style={styles.score}>
{team.totalCaches}/{totalCacheCount}
</Text>
</View> </View>
</Pressable> </Pressable>
{expandedTeam === team.tid && ( {expandedTeam === team.tid && (
<View> <View>
{team.members.map((m) => ( {team.members.map((m) => (
// Highlight the current user's row so they can identify themselves // Highlight the current user's row so they can identify themselves
<View key={m.uid} style={[styles.subRow, m.uid === session.currentUid && styles.selfSubRow]}> <View
key={m.uid}
style={[
styles.subRow,
m.uid === session.currentUid && styles.selfSubRow,
]}
>
<Text style={[styles.subName, { flex: 1 }]}> <Text style={[styles.subName, { flex: 1 }]}>
{m.name}{m.isLeader ? ' (Team Leader)' : ''} {m.name}
{m.isLeader ? " (Team Leader)" : ""}
</Text>
<Text style={styles.subScore}>
{m.caches}/{totalCacheCount}
</Text> </Text>
<Text style={styles.subScore}>{m.caches}/{totalCacheCount}</Text>
<Pressable <Pressable
style={styles.resetMiniButton} style={styles.resetMiniButton}
onPress={() => handleResetPlayer(m)} onPress={() => handleResetPlayer(m)}
@@ -263,20 +348,39 @@ const LeaderboardScreen = ({navigation, route}) => {
// Helper: render an expandable team card (player version) // Helper: render an expandable team card (player version)
const renderTeamCard = (team, index) => ( const renderTeamCard = (team, index) => (
<Pressable key={team.tid} onPress={() => setExpandedTeam(expandedTeam === team.tid ? null : team.tid)}> <Pressable
key={team.tid}
onPress={() =>
setExpandedTeam(expandedTeam === team.tid ? null : team.tid)
}
>
<Card> <Card>
<View style={styles.rankRow}> <View style={styles.rankRow}>
<Text style={styles.rank}>#{index + 1}</Text> <Text style={styles.rank}>#{index + 1}</Text>
<Text style={styles.rankName}>{team.name} ({team.code})</Text> <Text style={styles.rankName}>
<Text style={styles.score}>{team.totalCaches}/{totalCacheCount}</Text> {team.name} ({team.code})
</Text>
<Text style={styles.score}>
{team.totalCaches}/{totalCacheCount}
</Text>
</View> </View>
{expandedTeam === team.tid && team.members.map((m) => ( {expandedTeam === team.tid &&
team.members.map((m) => (
// Highlight the current user's row so they can identify themselves // Highlight the current user's row so they can identify themselves
<View key={m.uid} style={[styles.subRow, m.uid === session.currentUid && styles.selfSubRow]}> <View
key={m.uid}
style={[
styles.subRow,
m.uid === session.currentUid && styles.selfSubRow,
]}
>
<Text style={styles.subName}> <Text style={styles.subName}>
{m.name}{m.isLeader ? ' (Team Leader)' : ''} {m.name}
{m.isLeader ? " (Team Leader)" : ""}
</Text>
<Text style={styles.subScore}>
{m.caches}/{totalCacheCount}
</Text> </Text>
<Text style={styles.subScore}>{m.caches}/{totalCacheCount}</Text>
</View> </View>
))} ))}
</Card> </Card>
@@ -286,11 +390,16 @@ const LeaderboardScreen = ({navigation, route}) => {
// Helper: render an admin player card (with reset button) // Helper: render an admin player card (with reset button)
const renderAdminPlayerCard = (player, index) => ( const renderAdminPlayerCard = (player, index) => (
// Highlight the current user's card so they can identify themselves // Highlight the current user's card so they can identify themselves
<Card key={player.uid} style={player.uid === session.currentUid ? styles.selfCard : undefined}> <Card
key={player.uid}
style={player.uid === session.currentUid ? styles.selfCard : undefined}
>
<View style={styles.rankRow}> <View style={styles.rankRow}>
<Text style={styles.rank}>#{index + 1}</Text> <Text style={styles.rank}>#{index + 1}</Text>
<Text style={styles.rankName}>{player.name}</Text> <Text style={styles.rankName}>{player.name}</Text>
<Text style={styles.score}>{player.caches}/{totalCacheCount}</Text> <Text style={styles.score}>
{player.caches}/{totalCacheCount}
</Text>
<Pressable <Pressable
style={styles.resetMiniButton} style={styles.resetMiniButton}
onPress={() => handleResetPlayer(player)} onPress={() => handleResetPlayer(player)}
@@ -304,11 +413,16 @@ const LeaderboardScreen = ({navigation, route}) => {
// Helper: render a simple player card // Helper: render a simple player card
const renderPlayerCard = (player, index) => ( const renderPlayerCard = (player, index) => (
// Highlight the current user's card so they can identify themselves // Highlight the current user's card so they can identify themselves
<Card key={player.uid} style={player.uid === session.currentUid ? styles.selfCard : undefined}> <Card
key={player.uid}
style={player.uid === session.currentUid ? styles.selfCard : undefined}
>
<View style={styles.rankRow}> <View style={styles.rankRow}>
<Text style={styles.rank}>#{index + 1}</Text> <Text style={styles.rank}>#{index + 1}</Text>
<Text style={styles.rankName}>{player.name}</Text> <Text style={styles.rankName}>{player.name}</Text>
<Text style={styles.score}>{player.caches}/{totalCacheCount}</Text> <Text style={styles.score}>
{player.caches}/{totalCacheCount}
</Text>
</View> </View>
</Card> </Card>
); );
@@ -320,10 +434,14 @@ const LeaderboardScreen = ({navigation, route}) => {
return ( return (
<Screen style={styles.container} showBack={Boolean(selectedDepartment)}> <Screen style={styles.container} showBack={Boolean(selectedDepartment)}>
{selectedDepartment && ( {selectedDepartment && (
<Text style={styles.departmentTitle}>{selectedDepartment.SubGroupName}</Text> <Text style={styles.departmentTitle}>
{selectedDepartment.SubGroupName}
</Text>
)} )}
<ScrollView style={styles.listSection}> <ScrollView style={styles.listSection}>
{teamRankings.map((team, index) => renderAdminTeamCard(team, index))} {teamRankings.map((team, index) =>
renderAdminTeamCard(team, index),
)}
{teamRankings.length === 0 && ( {teamRankings.length === 0 && (
<Text style={styles.emptyText}>No teams yet.</Text> <Text style={styles.emptyText}>No teams yet.</Text>
)} )}
@@ -336,10 +454,14 @@ const LeaderboardScreen = ({navigation, route}) => {
return ( return (
<Screen style={styles.container} showBack={Boolean(selectedDepartment)}> <Screen style={styles.container} showBack={Boolean(selectedDepartment)}>
{selectedDepartment && ( {selectedDepartment && (
<Text style={styles.departmentTitle}>{selectedDepartment.SubGroupName}</Text> <Text style={styles.departmentTitle}>
{selectedDepartment.SubGroupName}
</Text>
)} )}
<ScrollView style={styles.listSection}> <ScrollView style={styles.listSection}>
{playerRankings.map((player, index) => renderAdminPlayerCard(player, index))} {playerRankings.map((player, index) =>
renderAdminPlayerCard(player, index),
)}
{playerRankings.length === 0 && ( {playerRankings.length === 0 && (
<Text style={styles.emptyText}>No players yet.</Text> <Text style={styles.emptyText}>No players yet.</Text>
)} )}
@@ -356,20 +478,40 @@ const LeaderboardScreen = ({navigation, route}) => {
<Screen style={styles.container}> <Screen style={styles.container}>
<View style={styles.tabRow}> <View style={styles.tabRow}>
<Pressable <Pressable
style={[styles.tabButton, activeTab === 'game' && styles.tabButtonActive]} style={[
onPress={() => setActiveTab('game')} styles.tabButton,
activeTab === "game" && styles.tabButtonActive,
]}
onPress={() => setActiveTab("game")}
> >
<Text style={[styles.tabLabel, activeTab === 'game' && styles.tabLabelActive]}>GAME</Text> <Text
style={[
styles.tabLabel,
activeTab === "game" && styles.tabLabelActive,
]}
>
GAME
</Text>
</Pressable> </Pressable>
<Pressable <Pressable
style={[styles.tabButton, activeTab === 'team' && styles.tabButtonActive]} style={[
onPress={() => setActiveTab('team')} styles.tabButton,
activeTab === "team" && styles.tabButtonActive,
]}
onPress={() => setActiveTab("team")}
> >
<Text style={[styles.tabLabel, activeTab === 'team' && styles.tabLabelActive]}>TEAM</Text> <Text
style={[
styles.tabLabel,
activeTab === "team" && styles.tabLabelActive,
]}
>
TEAM
</Text>
</Pressable> </Pressable>
</View> </View>
{activeTab === 'game' ? ( {activeTab === "game" ? (
<ScrollView style={styles.listSection}> <ScrollView style={styles.listSection}>
{teamRankings.map((team, index) => renderTeamCard(team, index))} {teamRankings.map((team, index) => renderTeamCard(team, index))}
{teamRankings.length === 0 && ( {teamRankings.length === 0 && (
@@ -380,18 +522,28 @@ const LeaderboardScreen = ({navigation, route}) => {
<ScrollView style={styles.listSection}> <ScrollView style={styles.listSection}>
{myTeamRanking.map((m, index) => ( {myTeamRanking.map((m, index) => (
// Highlight the current user's card so they can identify themselves // Highlight the current user's card so they can identify themselves
<Card key={m.uid} style={m.uid === session.currentUid ? styles.selfCard : undefined}> <Card
key={m.uid}
style={
m.uid === session.currentUid ? styles.selfCard : undefined
}
>
<View style={styles.rankRow}> <View style={styles.rankRow}>
<Text style={styles.rank}>#{index + 1}</Text> <Text style={styles.rank}>#{index + 1}</Text>
<Text style={styles.rankName}> <Text style={styles.rankName}>
{m.name}{m.isLeader ? ' (Team Leader)' : ''} {m.name}
{m.isLeader ? " (Team Leader)" : ""}
</Text>
<Text style={styles.score}>
{m.caches}/{totalCacheCount}
</Text> </Text>
<Text style={styles.score}>{m.caches}/{totalCacheCount}</Text>
</View> </View>
</Card> </Card>
))} ))}
{myTeamRanking.length === 0 && ( {myTeamRanking.length === 0 && (
<Text style={styles.emptyText}>Join a team to see your team leaderboard.</Text> <Text style={styles.emptyText}>
Join a team to see your team leaderboard.
</Text>
)} )}
</ScrollView> </ScrollView>
)} )}
@@ -413,62 +565,97 @@ const LeaderboardScreen = ({navigation, route}) => {
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
center: {justifyContent: 'center', alignItems: 'center'}, center: { justifyContent: "center", alignItems: "center" },
container: { padding: 0 }, container: { padding: 0 },
body: {color: '#bac2de', fontSize: 15}, body: { color: "#bac2de", fontSize: 15 },
departmentTitle: {fontSize: 20, fontWeight: '700', color: '#cdd6f4', paddingHorizontal: 15, paddingTop: 15, marginBottom: 6}, departmentTitle: {
openLabel: {fontSize: 13, fontWeight: '700', color: '#bd93f9'}, fontSize: 20,
fontWeight: "700",
color: "#cdd6f4",
paddingHorizontal: 15,
paddingTop: 15,
marginBottom: 6,
},
openLabel: { fontSize: 13, fontWeight: "700", color: "#bd93f9" },
tabRow: { tabRow: {
flexDirection: 'row', flexDirection: "row",
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: '#45475a', borderBottomColor: "#45475a",
}, },
tabButton: { tabButton: {
flex: 1, flex: 1,
paddingVertical: 12, paddingVertical: 12,
alignItems: 'center', alignItems: "center",
}, },
tabButtonActive: { tabButtonActive: {
borderBottomWidth: 2, borderBottomWidth: 2,
borderBottomColor: '#bd93f9', borderBottomColor: "#bd93f9",
}, },
tabLabel: {fontSize: 15, fontWeight: '600', color: '#6c7086'}, tabLabel: { fontSize: 15, fontWeight: "600", color: "#6c7086" },
tabLabelActive: {color: '#bd93f9'}, tabLabelActive: { color: "#bd93f9" },
listSection: { flex: 1, paddingHorizontal: 15, paddingTop: 10 }, listSection: { flex: 1, paddingHorizontal: 15, paddingTop: 10 },
rankRow: { rankRow: {
flexDirection: 'row', flexDirection: "row",
alignItems: 'center', alignItems: "center",
gap: 10, gap: 10,
}, },
rank: {fontSize: 16, fontWeight: '700', color: '#bac2de', width: 32}, rank: { fontSize: 16, fontWeight: "700", color: "#bac2de", width: 32 },
rankName: {flex: 1, fontSize: 15, fontWeight: '600', color: '#cdd6f4'}, rankName: { flex: 1, fontSize: 15, fontWeight: "600", color: "#cdd6f4" },
score: {fontSize: 14, fontWeight: '600', color: '#a6e3a1'}, score: { fontSize: 14, fontWeight: "600", color: "#a6e3a1" },
subRow: { subRow: {
flexDirection: 'row', flexDirection: "row",
alignItems: 'center', alignItems: "center",
paddingLeft: 42, paddingLeft: 42,
paddingVertical: 6, paddingVertical: 6,
borderTopWidth: 1, borderTopWidth: 1,
borderTopColor: '#45475a', borderTopColor: "#45475a",
}, },
subName: {fontSize: 13, color: '#bac2de'}, subName: { fontSize: 13, color: "#bac2de" },
subScore: {fontSize: 13, fontWeight: '600', color: '#a6e3a1', marginRight: 8}, subScore: {
emptyText: {color: '#6c7086', textAlign: 'center', marginTop: 30, fontSize: 14}, fontSize: 13,
deleteTeamWrap: {marginTop: 8, paddingLeft: 42, flexDirection: 'row', gap: 8}, fontWeight: "600",
resetTeamButton: {backgroundColor: '#f4a460', borderColor: '#f4a460', minHeight: 36, flex: 1}, color: "#a6e3a1",
deleteTeamButton: {backgroundColor: '#D92800', borderColor: '#D92800', minHeight: 36, flex: 1}, marginRight: 8,
deleteTeamLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 13}, },
deleteTeamDeleteLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13}, 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: { resetMiniButton: {
backgroundColor: '#86efac', backgroundColor: "#86efac",
borderRadius: 6, borderRadius: 6,
paddingHorizontal: 10, paddingHorizontal: 10,
paddingVertical: 4, paddingVertical: 4,
marginLeft: 6, marginLeft: 6,
}, },
resetMiniLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 12}, resetMiniLabel: { color: "#1e1e2e", fontWeight: "600", fontSize: 12 },
selfCard: {backgroundColor: 'rgba(166, 227, 161, 0.15)', borderColor: '#a6e3a1'}, selfCard: {
selfSubRow: {backgroundColor: 'rgba(166, 227, 161, 0.15)'}, backgroundColor: "rgba(166, 227, 161, 0.15)",
borderColor: "#a6e3a1",
},
selfSubRow: { backgroundColor: "rgba(166, 227, 161, 0.15)" },
}); });
export default LeaderboardScreen; export default LeaderboardScreen;
+27 -26
View File
@@ -1,11 +1,12 @@
import React, {useState} from 'react'; import { useState } from "react";
import {Pressable, StyleSheet, Text, TextInput, View} from 'react-native'; import { Pressable, StyleSheet, Text, TextInput, View } from "react-native";
import useAuthHook from '../../hooks/useAuthHook'; import useAuthHook from "../../hooks/useAuthHook";
import Screen from '../layout/Screen'; import Screen from "../layout/Screen";
import {Button, ButtonTray} from '../UI/Button'; import { Button, ButtonTray } from "../UI/Button";
const LoginScreen = ({ navigation }) => { 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 [submitting, setSubmitting] = useState(false);
const handleLogin = async () => { const handleLogin = async () => {
@@ -17,10 +18,10 @@ const LoginScreen = ({ navigation }) => {
} }
const rootNav = navigation.getParent() || navigation; const rootNav = navigation.getParent() || navigation;
if (user.IsAcceptedAdmin) { if (user.IsAcceptedAdmin) {
rootNav.reset({ index: 0, routes: [{ name: 'Admin' }] }); rootNav.reset({ index: 0, routes: [{ name: "Admin" }] });
return; return;
} }
rootNav.reset({ index: 0, routes: [{ name: 'Game' }] }); rootNav.reset({ index: 0, routes: [{ name: "Game" }] });
}; };
return ( return (
@@ -52,14 +53,14 @@ const LoginScreen = ({ navigation }) => {
<ButtonTray> <ButtonTray>
<Button <Button
label={submitting ? 'Logging in...' : 'Login'} label={submitting ? "Logging in..." : "Login"}
onClick={handleLogin} onClick={handleLogin}
styleButton={styles.submitButton} styleButton={styles.submitButton}
styleLabel={styles.submitText} styleLabel={styles.submitText}
/> />
</ButtonTray> </ButtonTray>
<Pressable onPress={() => navigation.navigate('RegisterScreen')}> <Pressable onPress={() => navigation.navigate("RegisterScreen")}>
<Text style={styles.linkText}>Create new account</Text> <Text style={styles.linkText}>Create new account</Text>
</Pressable> </Pressable>
</View> </View>
@@ -69,45 +70,45 @@ const LoginScreen = ({ navigation }) => {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
justifyContent: 'center', justifyContent: "center",
}, },
content: { content: {
gap: 15, gap: 15,
}, },
title: { title: {
fontSize: 24, fontSize: 24,
fontWeight: 'bold', fontWeight: "bold",
textAlign: 'center', textAlign: "center",
marginBottom: 10, marginBottom: 10,
color: '#cdd6f4', color: "#cdd6f4",
}, },
input: { input: {
borderWidth: 1, borderWidth: 1,
borderColor: '#45475a', borderColor: "#45475a",
borderRadius: 8, borderRadius: 8,
paddingHorizontal: 12, paddingHorizontal: 12,
paddingVertical: 10, paddingVertical: 10,
fontSize: 16, fontSize: 16,
color: '#cdd6f4', color: "#cdd6f4",
backgroundColor: '#313244', backgroundColor: "#313244",
}, },
errorText: { errorText: {
color: '#D92800', color: "#D92800",
textAlign: 'center', textAlign: "center",
marginTop: 5, marginTop: 5,
}, },
submitButton: { submitButton: {
backgroundColor: '#bd93f9', backgroundColor: "#bd93f9",
borderColor: '#bd93f9', borderColor: "#bd93f9",
}, },
submitText: { submitText: {
color: '#1e1e2e', color: "#1e1e2e",
fontWeight: '600', fontWeight: "600",
}, },
linkText: { linkText: {
textAlign: 'center', textAlign: "center",
color: '#bd93f9', color: "#bd93f9",
fontWeight: '600', fontWeight: "600",
paddingVertical: 10, paddingVertical: 10,
}, },
}); });
+184 -70
View File
@@ -1,22 +1,45 @@
import React, {useCallback, useEffect, useState} from 'react'; import { useCallback, useEffect, useState } from "react";
import {ActivityIndicator, Alert, Pressable, ScrollView, StyleSheet, Text, View} from 'react-native'; import {
import Screen from '../layout/Screen'; ActivityIndicator,
import Card from '../UI/Card'; Alert,
import {Button} from '../UI/Button'; Pressable,
import PendingApprovalView from '../UI/PendingApprovalView'; ScrollView,
import useGameHook from '../../hooks/useGameHook'; StyleSheet,
import {getSession} from '../../hooks/SessionStore'; 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 }) => { const PlayersScreen = ({ navigation, route }) => {
// Initialisation ------------ // Initialisation ------------
const session = getSession(); const session = getSession();
const {getTeams, getTeamMembers, getGroupMembers, removeMember, resetPlayerProgress, getUser, getAdminWaitlist, approveAdmin, rejectAdmin, getSubgroups} = useGameHook(); const {
getTeams,
getTeamMembers,
getGroupMembers,
removeMember,
resetPlayerProgress,
getUser,
getAdminWaitlist,
approveAdmin,
rejectAdmin,
getSubgroups,
} = useGameHook();
const isBusiness = Boolean(session.isBusiness); const isBusiness = Boolean(session.isBusiness);
const isAdmin = Boolean(session.isAcceptedAdmin); const isAdmin = Boolean(session.isAcceptedAdmin);
const selectedDepartment = route?.params?.department || null; const selectedDepartment = route?.params?.department || null;
const effectiveSGid = isBusiness const effectiveSGid = isBusiness
? ((isAdmin && selectedDepartment?.SGid) ? selectedDepartment.SGid : (isAdmin ? null : session.currentSGid)) ? isAdmin && selectedDepartment?.SGid
? selectedDepartment.SGid
: isAdmin
? null
: session.currentSGid
: null; : null;
// State ---------------------- // State ----------------------
@@ -31,7 +54,10 @@ const PlayersScreen = ({navigation, route}) => {
// Handlers ------------------- // Handlers -------------------
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
if (!session.currentGid) { setLoading(false); return; } if (!session.currentGid) {
setLoading(false);
return;
}
if (isBusiness && isAdmin && !selectedDepartment) { if (isBusiness && isAdmin && !selectedDepartment) {
const sgs = await getSubgroups(session.currentGid); const sgs = await getSubgroups(session.currentGid);
@@ -59,7 +85,9 @@ const PlayersScreen = ({navigation, route}) => {
}); });
} }
setAdminTeam(adminRows); setAdminTeam(adminRows);
setIsCurrentUserAdminLead(adminRows.length > 0 && adminRows[0].Uid === session.currentUid); setIsCurrentUserAdminLead(
adminRows.length > 0 && adminRows[0].Uid === session.currentUid,
);
setPlayers([]); setPlayers([]);
setLoading(false); setLoading(false);
return; return;
@@ -72,30 +100,39 @@ const PlayersScreen = ({navigation, route}) => {
let subgroupNameMap = {}; let subgroupNameMap = {};
if (isBusiness) { if (isBusiness) {
const sgs = await getSubgroups(session.currentGid); const sgs = await getSubgroups(session.currentGid);
for (const sg of (sgs || [])) { for (const sg of sgs || []) {
if (!sg.IsAdminGroup) subgroupNameMap[sg.SGid] = sg.SubGroupName; if (!sg.IsAdminGroup) subgroupNameMap[sg.SGid] = sg.SubGroupName;
} }
} }
// Get ALL members (including admins) // Get ALL members (including admins)
const allMembers = await getGroupMembers(session.currentGid, effectiveSGid ?? null); const allMembers = await getGroupMembers(
session.currentGid,
effectiveSGid ?? null,
);
// Get admin waitlist entries // Get admin waitlist entries
const waitlist = effectiveSGid ? [] : await getAdminWaitlist(session.currentGid); const waitlist = effectiveSGid
? []
: await getAdminWaitlist(session.currentGid);
// Deduplicate allMembers by Uid — a user may have two rows if they were a // 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. // regular member before being approved as an admin. Prefer the admin row.
const seenUids = new Set(); const seenUids = new Set();
const uniqueMembers = (allMembers || []) const uniqueMembers = (allMembers || [])
.sort((a, b) => (b.IsAcceptedAdmin ? 1 : 0) - (a.IsAcceptedAdmin ? 1 : 0)) .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; }); .filter((m) => {
if (seenUids.has(m.Uid)) return false;
seenUids.add(m.Uid);
return true;
});
// Build team lookup: { Uid -> { teamCode, teamName, isLeader } } // Build team lookup: { Uid -> { teamCode, teamName, isLeader } }
const allTeams = await getTeams(session.currentGid, effectiveSGid ?? null); const allTeams = await getTeams(session.currentGid, effectiveSGid ?? null);
const teamInfoByUid = {}; const teamInfoByUid = {};
for (const team of (allTeams || [])) { for (const team of allTeams || []) {
const tms = await getTeamMembers(team.Tid); const tms = await getTeamMembers(team.Tid);
for (const tm of (tms || [])) { for (const tm of tms || []) {
teamInfoByUid[tm.Uid] = { teamInfoByUid[tm.Uid] = {
teamCode: team.JoinCode, teamCode: team.JoinCode,
teamName: team.TeamName, teamName: team.TeamName,
@@ -117,10 +154,10 @@ const PlayersScreen = ({navigation, route}) => {
let adminTag = null; let adminTag = null;
let waitlistId = null; let waitlistId = null;
if (waitlistEntry) { if (waitlistEntry) {
adminTag = '[Admin Awaiting Response]'; adminTag = "[Admin Awaiting Response]";
waitlistId = waitlistEntry.id; waitlistId = waitlistEntry.id;
} else if (m.IsAcceptedAdmin) { } else if (m.IsAcceptedAdmin) {
adminTag = '[ADMIN]'; adminTag = "[ADMIN]";
} }
playerList.push({ playerList.push({
...user, ...user,
@@ -134,7 +171,7 @@ const PlayersScreen = ({navigation, route}) => {
} }
// Include waitlisted users who are not yet group members at all // Include waitlisted users who are not yet group members at all
for (const w of (waitlist || [])) { for (const w of waitlist || []) {
if (playerList.some((p) => p.Uid === w.Uid)) continue; if (playerList.some((p) => p.Uid === w.Uid)) continue;
if (w.Uid === session.currentUid) continue; if (w.Uid === session.currentUid) continue;
const user = await getUser(w.Uid); const user = await getUser(w.Uid);
@@ -143,7 +180,7 @@ const PlayersScreen = ({navigation, route}) => {
...user, ...user,
membershipId: null, membershipId: null,
team: null, team: null,
adminTag: '[Admin Awaiting Response]', adminTag: "[Admin Awaiting Response]",
waitlistId: w.id, waitlistId: w.id,
departmentName: null, departmentName: null,
}); });
@@ -152,9 +189,20 @@ const PlayersScreen = ({navigation, route}) => {
setPlayers(playerList); setPlayers(playerList);
setLoading(false); setLoading(false);
}, [session.currentGid, session.currentUid, session.currentSGid, isBusiness, isAdmin, selectedDepartment, selectedDepartment?.SGid, effectiveSGid]); }, [
session.currentGid,
session.currentUid,
session.currentSGid,
isBusiness,
isAdmin,
selectedDepartment,
selectedDepartment?.SGid,
effectiveSGid,
]);
useEffect(() => { loadData(); }, [loadData]); useEffect(() => {
loadData();
}, [loadData]);
const handleRemovePlayer = async (membershipId) => { const handleRemovePlayer = async (membershipId) => {
await removeMember(membershipId); await removeMember(membershipId);
@@ -163,16 +211,23 @@ const PlayersScreen = ({navigation, route}) => {
const handleResetPlayerProgress = (player) => { const handleResetPlayerProgress = (player) => {
Alert.alert( Alert.alert(
'Reset Progress', "Reset Progress",
`Reset all cache claims for ${player.username}?`, `Reset all cache claims for ${player.username}?`,
[ [
{text: 'Cancel', style: 'cancel'}, { text: "Cancel", style: "cancel" },
{ {
text: 'Reset', text: "Reset",
style: 'destructive', style: "destructive",
onPress: async () => { onPress: async () => {
await resetPlayerProgress(session.currentGid, player.Uid, effectiveSGid); await resetPlayerProgress(
Alert.alert('Done', `Progress for ${player.username} has been reset.`); session.currentGid,
player.Uid,
effectiveSGid,
);
Alert.alert(
"Done",
`Progress for ${player.username} has been reset.`,
);
}, },
}, },
], ],
@@ -192,15 +247,16 @@ const PlayersScreen = ({navigation, route}) => {
}; };
const handleKickAdmin = (adminMember) => { const handleKickAdmin = (adminMember) => {
if (!isCurrentUserAdminLead || adminMember.Uid === session.currentUid) return; if (!isCurrentUserAdminLead || adminMember.Uid === session.currentUid)
return;
Alert.alert( Alert.alert(
'Kick Admin', "Kick Admin",
`Remove ${adminMember.username} from the admin team?`, `Remove ${adminMember.username} from the admin team?`,
[ [
{text: 'Cancel', style: 'cancel'}, { text: "Cancel", style: "cancel" },
{ {
text: 'Kick', text: "Kick",
style: 'destructive', style: "destructive",
onPress: async () => { onPress: async () => {
await removeMember(adminMember.membershipId); await removeMember(adminMember.membershipId);
await loadData(); await loadData();
@@ -241,7 +297,9 @@ const PlayersScreen = ({navigation, route}) => {
<Pressable onPress={() => setAdminTeamExpanded((prev) => !prev)}> <Pressable onPress={() => setAdminTeamExpanded((prev) => !prev)}>
<View style={styles.playerRow}> <View style={styles.playerRow}>
<Text style={styles.playerName}>Admin Team</Text> <Text style={styles.playerName}>Admin Team</Text>
<Text style={styles.openLabel}>{adminTeamExpanded ? 'Close' : 'Open'}</Text> <Text style={styles.openLabel}>
{adminTeamExpanded ? "Close" : "Open"}
</Text>
</View> </View>
</Pressable> </Pressable>
{adminTeamExpanded && ( {adminTeamExpanded && (
@@ -249,9 +307,11 @@ const PlayersScreen = ({navigation, route}) => {
{adminTeam.map((adminMember, index) => ( {adminTeam.map((adminMember, index) => (
<View key={adminMember.Uid} style={styles.adminTeamRow}> <View key={adminMember.Uid} style={styles.adminTeamRow}>
<Text style={styles.adminTeamName} numberOfLines={1}> <Text style={styles.adminTeamName} numberOfLines={1}>
{adminMember.username}{index === 0 ? ' (Admin Lead)' : ''} {adminMember.username}
{index === 0 ? " (Admin Lead)" : ""}
</Text> </Text>
{isCurrentUserAdminLead && adminMember.Uid !== session.currentUid && ( {isCurrentUserAdminLead &&
adminMember.Uid !== session.currentUid && (
<Button <Button
label="Kick Admin" label="Kick Admin"
onClick={() => handleKickAdmin(adminMember)} onClick={() => handleKickAdmin(adminMember)}
@@ -270,11 +330,15 @@ const PlayersScreen = ({navigation, route}) => {
{departmentList.map((department) => ( {departmentList.map((department) => (
<Pressable <Pressable
key={department.SGid} key={department.SGid}
onPress={() => navigation.navigate('PlayersScreen', {department})} onPress={() =>
navigation.navigate("PlayersScreen", { department })
}
> >
<Card> <Card>
<View style={styles.playerRow}> <View style={styles.playerRow}>
<Text style={styles.playerName}>{department.SubGroupName}</Text> <Text style={styles.playerName}>
{department.SubGroupName}
</Text>
<Text style={styles.openLabel}>Open</Text> <Text style={styles.openLabel}>Open</Text>
</View> </View>
</Card> </Card>
@@ -294,19 +358,30 @@ const PlayersScreen = ({navigation, route}) => {
<ScrollView style={styles.listSection}> <ScrollView style={styles.listSection}>
{players.map((player) => ( {players.map((player) => (
// Waitlisted admins get a light red tint so they stand out from regular players // Waitlisted admins get a light red tint so they stand out from regular players
<Card key={player.Uid} style={player.waitlistId ? styles.waitlistCard : undefined}> <Card
key={player.Uid}
style={player.waitlistId ? styles.waitlistCard : undefined}
>
<View style={styles.playerRow}> <View style={styles.playerRow}>
<Text style={styles.playerName} numberOfLines={1}> <Text style={styles.playerName} numberOfLines={1}>
{player.username} {player.username}
{player.adminTag && ( {player.adminTag && (
<Text style={player.adminTag === '[ADMIN]' ? styles.adminBadge : styles.waitlistBadge}> <Text
{' '}{player.adminTag} style={
player.adminTag === "[ADMIN]"
? styles.adminBadge
: styles.waitlistBadge
}
>
{" "}
{player.adminTag}
</Text> </Text>
)} )}
{/* Show department name in brackets for non-admin players (org accounts) */} {/* Show department name in brackets for non-admin players (org accounts) */}
{!player.adminTag && player.departmentName && ( {!player.adminTag && player.departmentName && (
<Text style={styles.departmentName}> <Text style={styles.departmentName}>
{' '}({player.departmentName}) {" "}
({player.departmentName})
</Text> </Text>
)} )}
</Text> </Text>
@@ -315,7 +390,9 @@ const PlayersScreen = ({navigation, route}) => {
<Text style={styles.leaderBadge}>(Team Leader)</Text> <Text style={styles.leaderBadge}>(Team Leader)</Text>
)} )}
{player.team && ( {player.team && (
<Text style={styles.teamCodeBadge}>{player.team.teamCode}</Text> <Text style={styles.teamCodeBadge}>
{player.team.teamCode}
</Text>
)} )}
</View> </View>
</View> </View>
@@ -368,44 +445,81 @@ const PlayersScreen = ({navigation, route}) => {
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
center: {justifyContent: 'center', alignItems: 'center', flex: 1}, center: { justifyContent: "center", alignItems: "center", flex: 1 },
container: { padding: 0 }, container: { padding: 0 },
body: {color: '#bac2de', fontSize: 15}, body: { color: "#bac2de", fontSize: 15 },
sectionTitle: { sectionTitle: {
fontSize: 20, fontSize: 20,
fontWeight: '700', fontWeight: "700",
color: '#cdd6f4', color: "#cdd6f4",
marginBottom: 12, marginBottom: 12,
paddingHorizontal: 15, paddingHorizontal: 15,
paddingTop: 15, paddingTop: 15,
}, },
openLabel: {fontSize: 13, fontWeight: '700', color: '#bd93f9'}, openLabel: { fontSize: 13, fontWeight: "700", color: "#bd93f9" },
listSection: { flex: 1, paddingHorizontal: 15 }, listSection: { flex: 1, paddingHorizontal: 15 },
emptyText: {color: '#6c7086', textAlign: 'center', marginTop: 30, fontSize: 14}, emptyText: {
color: "#6c7086",
textAlign: "center",
marginTop: 30,
fontSize: 14,
},
playerRow: { playerRow: {
flexDirection: 'row', flexDirection: "row",
alignItems: 'center', alignItems: "center",
gap: 8, gap: 8,
marginBottom: 8, marginBottom: 8,
}, },
playerName: {fontSize: 15, fontWeight: '600', color: '#cdd6f4', flex: 1}, playerName: { fontSize: 15, fontWeight: "600", color: "#cdd6f4", flex: 1 },
playerMeta: {flexDirection: 'row', alignItems: 'center', gap: 6}, playerMeta: { flexDirection: "row", alignItems: "center", gap: 6 },
leaderBadge: {fontSize: 11, color: '#bd93f9', fontWeight: '700'}, leaderBadge: { fontSize: 11, color: "#bd93f9", fontWeight: "700" },
teamCodeBadge: {fontSize: 12, fontWeight: '700', color: '#a6e3a1', letterSpacing: 1}, teamCodeBadge: {
adminBadge: {fontSize: 12, fontWeight: '700', color: '#D92800'}, fontSize: 12,
waitlistBadge: {fontSize: 11, fontWeight: '600', color: '#f4a460'}, fontWeight: "700",
actionRow: {flexDirection: 'row', gap: 8}, color: "#a6e3a1",
resetButton: {backgroundColor: '#86efac', borderColor: '#86efac', minHeight: 36, flex: 1, paddingHorizontal: 10}, letterSpacing: 1,
removeButton: {backgroundColor: '#D92800', borderColor: '#D92800', minHeight: 36, flex: 1, paddingHorizontal: 10}, },
actionLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 13}, adminBadge: { fontSize: 12, fontWeight: "700", color: "#D92800" },
kickLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13}, 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 }, adminTeamWrap: { gap: 8 },
adminTeamRow: {flexDirection: 'row', alignItems: 'center', gap: 8}, adminTeamRow: { flexDirection: "row", alignItems: "center", gap: 8 },
adminTeamName: {flex: 1, fontSize: 14, color: '#cdd6f4', fontWeight: '600'}, adminTeamName: { flex: 1, fontSize: 14, color: "#cdd6f4", fontWeight: "600" },
departmentName: {fontSize: 14, color: '#6c7086', fontStyle: 'italic'}, departmentName: { fontSize: 14, color: "#6c7086", fontStyle: "italic" },
waitlistCard: {backgroundColor: 'rgba(217, 40, 0, 0.15)', borderColor: '#D92800'}, waitlistCard: {
approveButton: {backgroundColor: '#a6e3a1', borderColor: '#a6e3a1', minHeight: 36, flex: 1, paddingHorizontal: 10}, backgroundColor: "rgba(217, 40, 0, 0.15)",
denyButton: {backgroundColor: '#D92800', borderColor: '#D92800', minHeight: 36, flex: 1, paddingHorizontal: 10}, 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; export default PlayersScreen;
+67 -42
View File
@@ -1,14 +1,23 @@
import React, {useState} from 'react'; import { useState } from "react";
import {Pressable, StyleSheet, Text, TextInput, View} from 'react-native'; import { Pressable, StyleSheet, Text, TextInput, View } from "react-native";
import useAuthHook from '../../hooks/useAuthHook'; import useAuthHook from "../../hooks/useAuthHook";
import Screen from '../layout/Screen'; import Screen from "../layout/Screen";
import {Button, ButtonTray} from '../UI/Button'; import { Button, ButtonTray } from "../UI/Button";
const RegisterScreen = ({ navigation }) => { const RegisterScreen = ({ navigation }) => {
const { const {
username, email, password, confirmPassword, accountType, username,
error, isLoading, email,
setUsername, setEmail, setPassword, setConfirmPassword, setAccountType, password,
confirmPassword,
accountType,
error,
isLoading,
setUsername,
setEmail,
setPassword,
setConfirmPassword,
setAccountType,
register, register,
} = useAuthHook(); } = useAuthHook();
@@ -21,10 +30,10 @@ const RegisterScreen = ({navigation}) => {
if (!user) return; if (!user) return;
const rootNav = navigation.getParent() || navigation; const rootNav = navigation.getParent() || navigation;
if (user.IsAcceptedAdmin) { if (user.IsAcceptedAdmin) {
rootNav.reset({index: 0, routes: [{name: 'Admin'}]}); rootNav.reset({ index: 0, routes: [{ name: "Admin" }] });
return; return;
} }
rootNav.reset({index: 0, routes: [{name: 'Game'}]}); rootNav.reset({ index: 0, routes: [{ name: "Game" }] });
}; };
return ( return (
@@ -71,20 +80,36 @@ const RegisterScreen = ({navigation}) => {
<View style={styles.typeRow}> <View style={styles.typeRow}>
<Pressable <Pressable
style={[styles.typeButton, accountType === 'Individual' && styles.typeButtonActive]} style={[
onPress={() => setAccountType('Individual')} styles.typeButton,
accountType === "Individual" && styles.typeButtonActive,
]}
onPress={() => setAccountType("Individual")}
disabled={isLoading} disabled={isLoading}
> >
<Text style={[styles.typeText, accountType === 'Individual' && styles.typeTextActive]}> <Text
style={[
styles.typeText,
accountType === "Individual" && styles.typeTextActive,
]}
>
Individual Individual
</Text> </Text>
</Pressable> </Pressable>
<Pressable <Pressable
style={[styles.typeButton, accountType === 'Business/School' && styles.typeButtonActive]} style={[
onPress={() => setAccountType('Business/School')} styles.typeButton,
accountType === "Business/School" && styles.typeButtonActive,
]}
onPress={() => setAccountType("Business/School")}
disabled={isLoading} disabled={isLoading}
> >
<Text style={[styles.typeText, accountType === 'Business/School' && styles.typeTextActive]}> <Text
style={[
styles.typeText,
accountType === "Business/School" && styles.typeTextActive,
]}
>
Business Business
</Text> </Text>
</Pressable> </Pressable>
@@ -94,14 +119,14 @@ const RegisterScreen = ({navigation}) => {
<ButtonTray> <ButtonTray>
<Button <Button
label={submitting ? 'Creating...' : 'Create Account'} label={submitting ? "Creating..." : "Create Account"}
onClick={handleRegister} onClick={handleRegister}
styleButton={styles.submitButton} styleButton={styles.submitButton}
styleLabel={styles.submitText} styleLabel={styles.submitText}
/> />
</ButtonTray> </ButtonTray>
<Pressable onPress={() => navigation.navigate('LoginScreen')}> <Pressable onPress={() => navigation.navigate("LoginScreen")}>
<Text style={styles.linkText}>Back to Login</Text> <Text style={styles.linkText}>Back to Login</Text>
</Pressable> </Pressable>
</View> </View>
@@ -111,68 +136,68 @@ const RegisterScreen = ({navigation}) => {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
justifyContent: 'center', justifyContent: "center",
}, },
content: { content: {
gap: 12, gap: 12,
}, },
title: { title: {
fontSize: 24, fontSize: 24,
fontWeight: 'bold', fontWeight: "bold",
textAlign: 'center', textAlign: "center",
marginBottom: 10, marginBottom: 10,
color: '#cdd6f4', color: "#cdd6f4",
}, },
input: { input: {
borderWidth: 1, borderWidth: 1,
borderColor: '#45475a', borderColor: "#45475a",
borderRadius: 8, borderRadius: 8,
paddingHorizontal: 12, paddingHorizontal: 12,
paddingVertical: 10, paddingVertical: 10,
fontSize: 16, fontSize: 16,
color: '#cdd6f4', color: "#cdd6f4",
backgroundColor: '#313244', backgroundColor: "#313244",
}, },
typeRow: { typeRow: {
flexDirection: 'row', flexDirection: "row",
gap: 10, gap: 10,
}, },
typeButton: { typeButton: {
flex: 1, flex: 1,
borderWidth: 1, borderWidth: 1,
borderColor: '#45475a', borderColor: "#45475a",
borderRadius: 8, borderRadius: 8,
alignItems: 'center', alignItems: "center",
paddingVertical: 10, paddingVertical: 10,
}, },
typeButtonActive: { typeButtonActive: {
backgroundColor: '#bd93f9', backgroundColor: "#bd93f9",
borderColor: '#bd93f9', borderColor: "#bd93f9",
}, },
typeText: { typeText: {
color: '#bac2de', color: "#bac2de",
fontWeight: '600', fontWeight: "600",
}, },
typeTextActive: { typeTextActive: {
color: '#1e1e2e', color: "#1e1e2e",
}, },
errorText: { errorText: {
color: '#D92800', color: "#D92800",
textAlign: 'center', textAlign: "center",
marginTop: 5, marginTop: 5,
}, },
submitButton: { submitButton: {
backgroundColor: '#bd93f9', backgroundColor: "#bd93f9",
borderColor: '#bd93f9', borderColor: "#bd93f9",
}, },
submitText: { submitText: {
color: '#1e1e2e', color: "#1e1e2e",
fontWeight: '600', fontWeight: "600",
}, },
linkText: { linkText: {
textAlign: 'center', textAlign: "center",
color: '#bd93f9', color: "#bd93f9",
fontWeight: '600', fontWeight: "600",
paddingVertical: 10, paddingVertical: 10,
}, },
}); });
+199 -74
View File
@@ -1,20 +1,44 @@
import React, {useEffect, useState} from 'react'; import { useEffect, useState } from "react";
import {ActivityIndicator, ScrollView, StyleSheet, Text, TextInput, View} from 'react-native'; import {
import Screen from '../layout/Screen'; ActivityIndicator,
import Card from '../UI/Card'; ScrollView,
import {Button, ButtonTray} from '../UI/Button'; StyleSheet,
import PendingApprovalView from '../UI/PendingApprovalView'; Text,
import useGameHook from '../../hooks/useGameHook'; TextInput,
import {getSession, setPendingAdmin, setSessionTeam, setSessionUser} from '../../hooks/SessionStore'; 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 session = getSession();
const isAdmin = session.isAcceptedAdmin; const isAdmin = session.isAcceptedAdmin;
const isBusiness = Boolean(session.isBusiness); 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 [activeTid, setActiveTid] = useState(session.currentTid);
const [teamCode, setTeamCode] = useState(''); const [teamCode, setTeamCode] = useState("");
const [team, setTeam] = useState(null); const [team, setTeam] = useState(null);
const [members, setMembers] = useState([]); const [members, setMembers] = useState([]);
const [memberNames, setMemberNames] = useState({}); const [memberNames, setMemberNames] = useState({});
@@ -22,7 +46,7 @@ const TeamScreen = ({navigation}) => {
const [teamsDisabled, setTeamsDisabled] = useState(false); const [teamsDisabled, setTeamsDisabled] = useState(false);
const [onAdminWaitlist, setOnAdminWaitlist] = useState(false); const [onAdminWaitlist, setOnAdminWaitlist] = useState(false);
const [editingName, setEditingName] = useState(false); const [editingName, setEditingName] = useState(false);
const [teamNameDraft, setTeamNameDraft] = useState(''); const [teamNameDraft, setTeamNameDraft] = useState("");
const loadTeamData = async (tid) => { const loadTeamData = async (tid) => {
const t = await getTeam(tid); const t = await getTeam(tid);
@@ -30,7 +54,7 @@ const TeamScreen = ({navigation}) => {
const ms = await getTeamMembers(tid); const ms = await getTeamMembers(tid);
setMembers(ms || []); setMembers(ms || []);
const names = {}; const names = {};
for (const m of (ms || [])) { for (const m of ms || []) {
const u = await getUser(m.Uid); const u = await getUser(m.Uid);
if (u) names[m.Uid] = u.username; if (u) names[m.Uid] = u.username;
} }
@@ -39,7 +63,10 @@ const TeamScreen = ({navigation}) => {
useEffect(() => { useEffect(() => {
const load = async () => { const load = async () => {
if (!session.currentGid) { setLoading(false); return; } if (!session.currentGid) {
setLoading(false);
return;
}
const group = await getLobby(session.currentGid); const group = await getLobby(session.currentGid);
let teamsAreEnabled = Boolean(group?.TeamsEnabled); let teamsAreEnabled = Boolean(group?.TeamsEnabled);
@@ -55,10 +82,13 @@ const TeamScreen = ({navigation}) => {
setPendingAdmin(false); setPendingAdmin(false);
setSessionUser(freshUser); setSessionUser(freshUser);
setSessionTeam(freshUser.TGid ?? null); setSessionTeam(freshUser.TGid ?? null);
navigation.reset({index: 0, routes: [{name: 'Admin'}]}); navigation.reset({ index: 0, routes: [{ name: "Admin" }] });
return; return;
} }
const waitlist = await getAdminWaitlist(session.currentGid, session.currentUid); const waitlist = await getAdminWaitlist(
session.currentGid,
session.currentUid,
);
if (waitlist.length > 0) { if (waitlist.length > 0) {
setOnAdminWaitlist(true); setOnAdminWaitlist(true);
setLoading(false); setLoading(false);
@@ -75,7 +105,10 @@ const TeamScreen = ({navigation}) => {
setTeamsDisabled(false); setTeamsDisabled(false);
if (!activeTid) { if (!activeTid) {
const waitlist = await getAdminWaitlist(session.currentGid, session.currentUid); const waitlist = await getAdminWaitlist(
session.currentGid,
session.currentUid,
);
if (waitlist.length > 0) { if (waitlist.length > 0) {
setOnAdminWaitlist(true); setOnAdminWaitlist(true);
setLoading(false); setLoading(false);
@@ -103,11 +136,14 @@ const TeamScreen = ({navigation}) => {
setPendingAdmin(false); setPendingAdmin(false);
setSessionUser(freshUser); setSessionUser(freshUser);
setSessionTeam(freshUser.TGid ?? null); setSessionTeam(freshUser.TGid ?? null);
navigation.reset({index: 0, routes: [{name: 'Admin'}]}); navigation.reset({ index: 0, routes: [{ name: "Admin" }] });
return; return;
} }
const waitlist = await getAdminWaitlist(currentSession.currentGid, currentSession.currentUid); const waitlist = await getAdminWaitlist(
currentSession.currentGid,
currentSession.currentUid,
);
if (stopped) return; if (stopped) return;
const isStillWaiting = waitlist.length > 0; const isStillWaiting = waitlist.length > 0;
@@ -150,9 +186,16 @@ const TeamScreen = ({navigation}) => {
const handleCreateTeam = async () => { const handleCreateTeam = async () => {
if (!session.currentGid) return; if (!session.currentGid) return;
const result = await createTeam({Gid: session.currentGid, SGid: session.currentSGid, TeamName: 'My Team'}); const result = await createTeam({
Gid: session.currentGid,
SGid: session.currentSGid,
TeamName: "My Team",
});
if (!result) return; if (!result) return;
const membership = await joinTeamByCode({Tid: result.Tid, Uid: session.currentUid}); const membership = await joinTeamByCode({
Tid: result.Tid,
Uid: session.currentUid,
});
if (membership) { if (membership) {
setSessionTeam(result.Tid); setSessionTeam(result.Tid);
setActiveTid(result.Tid); setActiveTid(result.Tid);
@@ -205,7 +248,9 @@ const TeamScreen = ({navigation}) => {
<Screen style={styles.center}> <Screen style={styles.center}>
<View style={styles.disabledWrap}> <View style={styles.disabledWrap}>
<Text style={styles.disabledTitle}>Teams Disabled</Text> <Text style={styles.disabledTitle}>Teams Disabled</Text>
<Text style={styles.disabledBody}>The admin has not enabled teams for this game.</Text> <Text style={styles.disabledBody}>
The admin has not enabled teams for this game.
</Text>
</View> </View>
</Screen> </Screen>
); );
@@ -213,7 +258,9 @@ const TeamScreen = ({navigation}) => {
// Waiting for admin approval // Waiting for admin approval
if (onAdminWaitlist) { if (onAdminWaitlist) {
return <PendingApprovalView message="Waiting to be approved for Admin Team"/>; return (
<PendingApprovalView message="Waiting to be approved for Admin Team" />
);
} }
// Not in a team // Not in a team
@@ -224,7 +271,9 @@ const TeamScreen = ({navigation}) => {
return ( return (
<Screen style={styles.center}> <Screen style={styles.center}>
{notInGame && ( {notInGame && (
<Text style={styles.notInGameText}>Join a game first to manage teams.</Text> <Text style={styles.notInGameText}>
Join a game first to manage teams.
</Text>
)} )}
<View style={styles.inputRow}> <View style={styles.inputRow}>
<TextInput <TextInput
@@ -296,11 +345,16 @@ const TeamScreen = ({navigation}) => {
</View> </View>
) : ( ) : (
<View style={styles.renameRow}> <View style={styles.renameRow}>
<Text style={styles.teamNameText}>{team.TeamName || 'Unnamed Team'}</Text> <Text style={styles.teamNameText}>
{team.TeamName || "Unnamed Team"}
</Text>
{isCurrentUserLeader && ( {isCurrentUserLeader && (
<Button <Button
label="Rename" label="Rename"
onClick={() => { setTeamNameDraft(team.TeamName || ''); setEditingName(true); }} onClick={() => {
setTeamNameDraft(team.TeamName || "");
setEditingName(true);
}}
styleButton={styles.renameButton} styleButton={styles.renameButton}
styleLabel={styles.renameLabel} styleLabel={styles.renameLabel}
/> />
@@ -312,13 +366,18 @@ const TeamScreen = ({navigation}) => {
<ScrollView style={styles.memberList}> <ScrollView style={styles.memberList}>
{members.map((m) => ( {members.map((m) => (
// Highlight the current user's card so they can identify themselves // Highlight the current user's card so they can identify themselves
<Card key={m.id} style={m.Uid === session.currentUid ? styles.selfCard : undefined}> <Card
key={m.id}
style={m.Uid === session.currentUid ? styles.selfCard : undefined}
>
<View style={styles.memberRow}> <View style={styles.memberRow}>
<View style={styles.memberInfo}> <View style={styles.memberInfo}>
<Text style={styles.memberName}> <Text style={styles.memberName}>
{memberNames[m.Uid] || `Player ${m.Uid}`} {memberNames[m.Uid] || `Player ${m.Uid}`}
</Text> </Text>
{m.IsLeader && <Text style={styles.leaderBadge}>(Team Leader)</Text>} {m.IsLeader && (
<Text style={styles.leaderBadge}>(Team Leader)</Text>
)}
</View> </View>
{isCurrentUserLeader && m.Uid !== session.currentUid && ( {isCurrentUserLeader && m.Uid !== session.currentUid && (
<Button <Button
@@ -349,83 +408,149 @@ const TeamScreen = ({navigation}) => {
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
center: {justifyContent: 'center', alignItems: 'center'}, center: { justifyContent: "center", alignItems: "center" },
container: { padding: 0 }, container: { padding: 0 },
disabledWrap: {paddingHorizontal: 30, alignItems: 'center', opacity: 0.45}, disabledWrap: { paddingHorizontal: 30, alignItems: "center", opacity: 0.45 },
disabledTitle: {fontSize: 20, fontWeight: '700', color: '#ffffff', marginBottom: 10, textAlign: 'center'}, disabledTitle: {
disabledBody: {fontSize: 15, color: '#ffffff', textAlign: 'center'}, fontSize: 20,
notInGameText: {color: '#ffffff', fontSize: 15, fontWeight: '600', marginBottom: 16, textAlign: 'center', paddingHorizontal: 20}, fontWeight: "700",
inputDisabled: {backgroundColor: '#313244', opacity: 0.5}, color: "#ffffff",
inputRow: {flexDirection: 'row', gap: 10, marginBottom: 15, width: '100%', paddingHorizontal: 20}, marginBottom: 10,
fullRow: {width: '100%', paddingHorizontal: 20}, 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: { codeInput: {
flex: 1, flex: 1,
borderWidth: 1, borderWidth: 1,
borderColor: '#45475a', borderColor: "#45475a",
borderRadius: 8, borderRadius: 8,
paddingHorizontal: 12, paddingHorizontal: 12,
paddingVertical: 10, paddingVertical: 10,
fontSize: 16, fontSize: 16,
color: '#ffffff', color: "#ffffff",
backgroundColor: '#313244', backgroundColor: "#313244",
}, },
joinButton: {backgroundColor: '#bd93f9', borderColor: '#bd93f9', flex: 0, paddingHorizontal: 20}, joinButton: {
joinLabel: {color: '#1e1e2e', fontWeight: '600'}, backgroundColor: "#bd93f9",
createButton: {backgroundColor: '#a6e3a1', borderColor: '#a6e3a1'}, borderColor: "#bd93f9",
createLabel: {color: '#1e1e2e', fontWeight: '600'}, flex: 0,
paddingHorizontal: 20,
},
joinLabel: { color: "#1e1e2e", fontWeight: "600" },
createButton: { backgroundColor: "#a6e3a1", borderColor: "#a6e3a1" },
createLabel: { color: "#1e1e2e", fontWeight: "600" },
waitlistText: { waitlistText: {
fontSize: 18, fontSize: 18,
fontWeight: '700', fontWeight: "700",
color: '#ffffff', color: "#ffffff",
textAlign: 'center', textAlign: "center",
paddingHorizontal: 30, paddingHorizontal: 30,
}, },
teamHeader: { teamHeader: {
backgroundColor: '#45475a', backgroundColor: "#45475a",
paddingVertical: 14, paddingVertical: 14,
paddingHorizontal: 16, paddingHorizontal: 16,
flexDirection: 'row', flexDirection: "row",
alignItems: 'center', alignItems: "center",
gap: 8, gap: 8,
}, },
teamCodeLabel: {color: '#ffffff', fontSize: 15, fontWeight: '600'}, teamCodeLabel: { color: "#ffffff", fontSize: 15, fontWeight: "600" },
teamCodeValue: {color: '#ffffff', fontSize: 17, fontWeight: '700', letterSpacing: 2}, teamCodeValue: {
teamNameSection: {paddingHorizontal: 15, paddingVertical: 10, borderBottomWidth: 1, borderBottomColor: '#45475a'}, color: "#ffffff",
teamNameText: {fontSize: 18, fontWeight: '700', color: '#ffffff', flex: 1}, fontSize: 17,
renameRow: {flexDirection: 'row', alignItems: 'center', gap: 8}, 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: { renameInput: {
flex: 1, flex: 1,
borderWidth: 1, borderWidth: 1,
borderColor: '#45475a', borderColor: "#45475a",
borderRadius: 8, borderRadius: 8,
paddingHorizontal: 12, paddingHorizontal: 12,
paddingVertical: 8, paddingVertical: 8,
fontSize: 16, fontSize: 16,
color: '#ffffff', color: "#ffffff",
backgroundColor: '#313244', backgroundColor: "#313244",
}, },
renameButton: {backgroundColor: '#bd93f9', borderColor: '#bd93f9', flex: 0, minHeight: 36, paddingHorizontal: 14}, renameButton: {
renameLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 13}, backgroundColor: "#bd93f9",
renameSaveButton: {backgroundColor: '#a6e3a1', borderColor: '#a6e3a1', flex: 0, minHeight: 36, paddingHorizontal: 12}, borderColor: "#bd93f9",
renameSaveLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 13}, flex: 0,
renameCancelButton: {backgroundColor: '#6c7086', borderColor: '#6c7086', flex: 0, minHeight: 36, paddingHorizontal: 12}, minHeight: 36,
renameCancelLabel: {color: '#cdd6f4', fontWeight: '600', fontSize: 13}, 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 }, memberList: { flex: 1, paddingHorizontal: 15, paddingTop: 10 },
memberRow: { memberRow: {
flexDirection: 'row', flexDirection: "row",
justifyContent: 'space-between', justifyContent: "space-between",
alignItems: 'center', 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,
}, },
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 }, leaveWrap: { paddingHorizontal: 0, paddingTop: 10, paddingBottom: 16 },
leaveButton: {backgroundColor: '#D92800', borderColor: '#D92800'}, leaveButton: { backgroundColor: "#D92800", borderColor: "#D92800" },
leaveLabel: {color: '#1e1e2e', fontWeight: '600'}, leaveLabel: { color: "#1e1e2e", fontWeight: "600" },
selfCard: {backgroundColor: 'rgba(166, 227, 161, 0.15)', borderColor: '#a6e3a1'}, selfCard: {
backgroundColor: "rgba(166, 227, 161, 0.15)",
borderColor: "#a6e3a1",
},
}); });
export default TeamScreen; export default TeamScreen;
@@ -27,17 +27,19 @@ const GlobalMapScreen = ({ navigation, route }) => {
const session = getSession(); const session = getSession();
const routeEvent = route?.params?.event ?? null; const routeEvent = route?.params?.event ?? null;
const eventId = const eventId =
route?.params?.eventId ?? routeEvent?.EventID ?? session.currentGlobalEventId; route?.params?.eventId ??
const { getCachesByEvent, getFindsByPlayer, logFind } = useGlobalHook(); routeEvent?.EventID ??
session.currentGlobalEventId;
const { getCachesByEvent, getFindsByPlayer, claimCache } = useGlobalHook();
const globalApiRef = useRef({ const globalApiRef = useRef({
getCachesByEvent, getCachesByEvent,
getFindsByPlayer, getFindsByPlayer,
logFind, claimCache,
}); });
globalApiRef.current = { globalApiRef.current = {
getCachesByEvent, getCachesByEvent,
getFindsByPlayer, getFindsByPlayer,
logFind, claimCache,
}; };
// State ------------------------------- // State -------------------------------
@@ -53,7 +55,8 @@ const GlobalMapScreen = ({ navigation, route }) => {
route?.params?.selectedCacheId ?? route?.params?.cache?.CacheID ?? null, route?.params?.selectedCacheId ?? route?.params?.cache?.CacheID ?? null,
); );
const [claimedPopupVisible, setClaimedPopupVisible] = useState(false); const [claimedPopupVisible, setClaimedPopupVisible] = useState(false);
const [claimedPopupMessage, setClaimedPopupMessage] = useState("Cache Claimed!"); const [claimedPopupMessage, setClaimedPopupMessage] =
useState("Cache Claimed!");
const claimableCaches = useMemo( const claimableCaches = useMemo(
() => () =>
@@ -126,24 +129,29 @@ const GlobalMapScreen = ({ navigation, route }) => {
(cache) => String(cache.CacheID) === String(cacheId), (cache) => String(cache.CacheID) === String(cacheId),
); );
const result = await globalApiRef.current.logFind({ const result = await globalApiRef.current.claimCache(
FindPlayerID: session.currentGlobalPlayerId, session.currentGlobalPlayerId,
FindCacheID: claimedCache?.CacheID ?? cacheId, claimedCache,
FindDatetime: new Date().toISOString(), );
});
setIsClaiming(false); setIsClaiming(false);
if (result) { 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"; const cacheName = claimedCache?.CacheName || "Cache";
const points = Number(claimedCache?.CachePoints ?? 0); setClaimedPopupMessage(
setClaimedPopupMessage(`${cacheName} claimed! You gained +${points} points.`); `${cacheName} claimed! You gained +${result.points} points.`,
);
setClaimedPopupVisible(true); setClaimedPopupVisible(true);
setTimeout(() => setClaimedPopupVisible(false), 3000); setTimeout(() => setClaimedPopupVisible(false), 3000);
await loadData({ forceRefresh: true });
}
}, },
[caches, foundCacheIds, loadData, session.currentGlobalPlayerId, setIsClaiming], [caches, foundCacheIds, session.currentGlobalPlayerId, setIsClaiming],
); );
const handleCacheSelect = (cache) => { const handleCacheSelect = (cache) => {
@@ -234,7 +242,9 @@ const GlobalMapScreen = ({ navigation, route }) => {
useEffect(() => { useEffect(() => {
if (selectedCacheId) return; 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]); }, [claimableCaches, route?.params?.cache?.CacheID, selectedCacheId]);
useEffect(() => { useEffect(() => {
@@ -335,8 +345,9 @@ const GlobalMapScreen = ({ navigation, route }) => {
region={mapRegion} region={mapRegion}
showsUserLocation showsUserLocation
> >
{(caches || []).map((cache) => { {(caches || [])
const found = foundCacheIds.includes(String(cache.CacheID)); .filter((cache) => !foundCacheIds.includes(String(cache.CacheID)))
.map((cache) => {
const selected = selectedCacheId === cache.CacheID; const selected = selectedCacheId === cache.CacheID;
return ( return (
<Marker <Marker
@@ -346,17 +357,15 @@ const GlobalMapScreen = ({ navigation, route }) => {
longitude: cache.CacheLongitude, longitude: cache.CacheLongitude,
}} }}
title={cache.CacheName} title={cache.CacheName}
description={`${cache.CachePoints ?? 0} pts${found ? " · Found" : ""}`} description={`${cache.CachePoints ?? 0} pts`}
pinColor={ pinColor={selected ? "#f59e0b" : "#ef4444"}
found ? "#22c55e" : selected ? "#f59e0b" : "#ef4444"
}
onPress={() => setSelectedCacheId(cache.CacheID)} onPress={() => setSelectedCacheId(cache.CacheID)}
onCalloutPress={() => onCalloutPress={() =>
navigation.navigate("GlobalCacheViewScreen", { navigation.navigate("GlobalCacheViewScreen", {
cache, cache,
event: routeEvent, event: routeEvent,
eventId, eventId,
isFound: found, isFound: false,
}) })
} }
/> />
@@ -383,7 +392,9 @@ const GlobalMapScreen = ({ navigation, route }) => {
</MapView> </MapView>
) : ( ) : (
<CacheList <CacheList
caches={caches} caches={(caches || []).filter(
(cache) => !foundCacheIds.includes(String(cache.CacheID)),
)}
foundCacheIds={foundCacheIds} foundCacheIds={foundCacheIds}
onSelect={handleCacheSelect} onSelect={handleCacheSelect}
/> />
+40 -140
View File
@@ -1,12 +1,6 @@
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import API from "../components/API/API"; import API from "../components/API/API";
import globalApiConfig from "../components/API/api.json"; import globalApiConfig from "../components/API/api.json";
import {
clearGlobalDataCache,
clearGlobalDataCacheByPrefix,
getOrFetchGlobalData,
writeGlobalDataCache,
} from "../utils/globalDataCache";
const GLOBAL_BASE = String( const GLOBAL_BASE = String(
process.env.EXPO_PUBLIC_GLOBAL_API_BASE || process.env.EXPO_PUBLIC_GLOBAL_API_BASE ||
@@ -24,26 +18,6 @@ const DEFAULT_GLOBAL_PROFILE_IMAGE_URL =
const isGlobalApiReady = () => Boolean(GLOBAL_BASE); 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. // Appends the API key as a query param when provided.
const withKey = (url) => { const withKey = (url) => {
if (!GLOBAL_KEY) return url; if (!GLOBAL_KEY) return url;
@@ -155,57 +129,34 @@ const useGlobalHook = () => {
// Handlers ---------------------------- // Handlers ----------------------------
// Events // Events
const getPublicEvents = useCallback( const getPublicEvents = useCallback(async () => {
async (options = {}) => {
if (!isGlobalApiReady()) return []; if (!isGlobalApiReady()) return [];
return getOrFetchGlobalData(
CACHE_KEY.publicEvents,
async () => {
const response = await API.get(withKey(endpoints.events)); const response = await API.get(withKey(endpoints.events));
if (!response.isSuccess) return []; if (!response.isSuccess) return [];
const rows = unwrapList(response.result); const rows = unwrapList(response.result);
return rows.filter((e) => e.EventIspublic); return rows.filter((e) => e.EventIspublic);
}, }, [endpoints.events]);
{ ttlMs: CACHE_TTL.events, forceRefresh: options.forceRefresh },
);
},
[endpoints.events],
);
const getEvent = useCallback( const getEvent = useCallback(
async (eventId, options = {}) => { async (eventId) => {
if (!isGlobalApiReady()) return null; if (!isGlobalApiReady()) return null;
return getOrFetchGlobalData( const response = await API.get(withKey(`${endpoints.events}/${eventId}`));
CACHE_KEY.event(eventId),
async () => {
const response = await API.get(
withKey(`${endpoints.events}/${eventId}`),
);
if (!response.isSuccess) return null; if (!response.isSuccess) return null;
return unwrapSingle(response.result); return unwrapSingle(response.result);
}, },
{ ttlMs: CACHE_TTL.events, forceRefresh: options.forceRefresh },
);
},
[endpoints.events], [endpoints.events],
); );
// Players // Players
const getPlayersByEvent = useCallback( const getPlayersByEvent = useCallback(
async (eventId, options = {}) => { async (eventId) => {
if (!isGlobalApiReady()) return []; if (!isGlobalApiReady()) return [];
return getOrFetchGlobalData(
CACHE_KEY.playersByEvent(eventId),
async () => {
const response = await API.get( const response = await API.get(
withKey(`${endpoints.players}/events/${eventId}`), withKey(`${endpoints.players}/events/${eventId}`),
); );
if (!response.isSuccess) return []; if (!response.isSuccess) return [];
return unwrapList(response.result); return unwrapList(response.result);
}, },
{ ttlMs: CACHE_TTL.players, forceRefresh: options.forceRefresh },
);
},
[endpoints.players], [endpoints.players],
); );
@@ -214,87 +165,56 @@ const useGlobalHook = () => {
if (!isGlobalApiReady()) return null; if (!isGlobalApiReady()) return null;
const response = await API.post(withKey(endpoints.players), data); const response = await API.post(withKey(endpoints.players), data);
if (!response.isSuccess) return null; if (!response.isSuccess) return null;
const joined = unwrapSingle(response.result); return unwrapSingle(response.result);
const eventId = data?.PlayerEventID ?? joined?.PlayerEventID;
if (eventId !== undefined && eventId !== null) {
clearGlobalDataCache(CACHE_KEY.playersByEvent(eventId));
}
return joined;
}, },
[endpoints.players], [endpoints.players],
); );
// Caches // Caches
const getCachesByEvent = useCallback( const getCachesByEvent = useCallback(
async (eventId, options = {}) => { async (eventId) => {
if (!isGlobalApiReady()) return []; if (!isGlobalApiReady()) return [];
return getOrFetchGlobalData(
CACHE_KEY.cachesByEvent(eventId),
async () => {
const response = await API.get( const response = await API.get(
withKey(`${endpoints.caches}/events/${eventId}`), withKey(`${endpoints.caches}/events/${eventId}`),
); );
if (!response.isSuccess) return []; if (!response.isSuccess) return [];
return unwrapList(response.result); return unwrapList(response.result);
}, },
{ ttlMs: CACHE_TTL.caches, forceRefresh: options.forceRefresh },
);
},
[endpoints.caches], [endpoints.caches],
); );
const getCache = useCallback( const getCache = useCallback(
async (cacheId, options = {}) => { async (cacheId) => {
if (!isGlobalApiReady()) return null; if (!isGlobalApiReady()) return null;
return getOrFetchGlobalData( const response = await API.get(withKey(`${endpoints.caches}/${cacheId}`));
CACHE_KEY.cacheById(cacheId),
async () => {
const response = await API.get(
withKey(`${endpoints.caches}/${cacheId}`),
);
if (!response.isSuccess) return null; if (!response.isSuccess) return null;
return unwrapSingle(response.result); return unwrapSingle(response.result);
}, },
{ ttlMs: CACHE_TTL.caches, forceRefresh: options.forceRefresh },
);
},
[endpoints.caches], [endpoints.caches],
); );
// Finds // Finds
const getFindsByEvent = useCallback( const getFindsByEvent = useCallback(
async (eventId, options = {}) => { async (eventId) => {
if (!isGlobalApiReady()) return []; if (!isGlobalApiReady()) return [];
return getOrFetchGlobalData(
CACHE_KEY.findsByEvent(eventId),
async () => {
const response = await API.get( const response = await API.get(
withKey(`${endpoints.finds}/events/${eventId}`), withKey(`${endpoints.finds}/events/${eventId}`),
); );
if (!response.isSuccess) return []; if (!response.isSuccess) return [];
return unwrapList(response.result); return unwrapList(response.result);
}, },
{ ttlMs: CACHE_TTL.finds, forceRefresh: options.forceRefresh },
);
},
[endpoints.finds], [endpoints.finds],
); );
const getFindsByPlayer = useCallback( const getFindsByPlayer = useCallback(
async (playerId, options = {}) => { async (playerId) => {
if (!isGlobalApiReady()) return []; if (!isGlobalApiReady()) return [];
return getOrFetchGlobalData(
CACHE_KEY.findsByPlayer(playerId),
async () => {
const response = await API.get( const response = await API.get(
withKey(`${endpoints.finds}/players/${playerId}`), withKey(`${endpoints.finds}/players/${playerId}`),
); );
if (!response.isSuccess) return []; if (!response.isSuccess) return [];
return unwrapList(response.result); return unwrapList(response.result);
}, },
{ ttlMs: CACHE_TTL.finds, forceRefresh: options.forceRefresh },
);
},
[endpoints.finds], [endpoints.finds],
); );
@@ -303,42 +223,42 @@ const useGlobalHook = () => {
if (!isGlobalApiReady()) return null; if (!isGlobalApiReady()) return null;
const response = await API.post(withKey(endpoints.finds), data); const response = await API.post(withKey(endpoints.finds), data);
if (!response.isSuccess) return null; if (!response.isSuccess) return null;
const find = unwrapSingle(response.result) || { ...data }; return unwrapSingle(response.result) || { ...data };
if (data?.FindPlayerID !== undefined && data?.FindPlayerID !== null) { },
clearGlobalDataCache(CACHE_KEY.findsByPlayer(data.FindPlayerID)); [endpoints.finds],
} );
// Event ID is not always present in payload, so clear all event-level find caches.
clearGlobalDataCacheByPrefix("global:finds:event:"); const claimCache = useCallback(
return find; 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], [endpoints.finds],
); );
// Users // Users
const getUsers = useCallback( const getUsers = useCallback(async () => {
async (options = {}) => {
if (!isGlobalApiReady()) return []; if (!isGlobalApiReady()) return [];
return getOrFetchGlobalData(
CACHE_KEY.users,
async () => {
const response = await API.get(withKey(endpoints.users)); const response = await API.get(withKey(endpoints.users));
if (!response.isSuccess) return []; if (!response.isSuccess) return [];
return unwrapList(response.result); return unwrapList(response.result);
}, }, [endpoints.users]);
{ ttlMs: CACHE_TTL.users, forceRefresh: options.forceRefresh },
);
},
[endpoints.users],
);
const getUser = useCallback( const getUser = useCallback(
async (userId, options = {}) => { async (userId) => {
if (!isGlobalApiReady()) return null; if (!isGlobalApiReady()) return null;
const key = CACHE_KEY.userById(userId);
return getOrFetchGlobalData(
key,
async () => {
// Some global API deployments expose list-only user routes. // Some global API deployments expose list-only user routes.
const byIdResponse = await API.get( const byIdResponse = await API.get(
withKey(`${endpoints.users}/${userId}`), withKey(`${endpoints.users}/${userId}`),
@@ -347,15 +267,11 @@ const useGlobalHook = () => {
const single = unwrapSingle(byIdResponse.result); const single = unwrapSingle(byIdResponse.result);
if (single) return single; if (single) return single;
} }
const users = await getUsers();
const users = await getUsers({ forceRefresh: options.forceRefresh });
return ( return (
users.find((user) => String(user.UserID) === String(userId)) || null users.find((user) => String(user.UserID) === String(userId)) || null
); );
}, },
{ ttlMs: CACHE_TTL.users, forceRefresh: options.forceRefresh },
);
},
[endpoints.users, getUsers], [endpoints.users, getUsers],
); );
@@ -364,17 +280,7 @@ const useGlobalHook = () => {
if (!isGlobalApiReady()) return null; if (!isGlobalApiReady()) return null;
const response = await API.post(withKey(endpoints.users), payload); const response = await API.post(withKey(endpoints.users), payload);
if (!response.isSuccess) return null; if (!response.isSuccess) return null;
const created = unwrapSingle(response.result); return 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;
}, },
[endpoints.users], [endpoints.users],
); );
@@ -387,14 +293,7 @@ const useGlobalHook = () => {
payload, payload,
); );
if (!response.isSuccess) return null; if (!response.isSuccess) return null;
const updated = unwrapSingle(response.result) || payload; return unwrapSingle(response.result) || payload;
writeGlobalDataCache(
CACHE_KEY.userById(userId),
updated,
CACHE_TTL.users,
);
clearGlobalDataCache(CACHE_KEY.users);
return updated;
}, },
[endpoints.users], [endpoints.users],
); );
@@ -420,7 +319,7 @@ const useGlobalHook = () => {
if (created) return created; if (created) return created;
// Final fallback: check list endpoint again after attempted creation. // Final fallback: check list endpoint again after attempted creation.
const refreshed = await getUsers({ forceRefresh: true }); const refreshed = await getUsers();
return ( return (
refreshed.find( refreshed.find(
(user) => String(user.UserUsername) === String(seed.UserUsername), (user) => String(user.UserUsername) === String(seed.UserUsername),
@@ -446,6 +345,7 @@ const useGlobalHook = () => {
getFindsByEvent, getFindsByEvent,
getFindsByPlayer, getFindsByPlayer,
logFind, logFind,
claimCache,
getUser, getUser,
createUser, createUser,
updateUser, updateUser,