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
+1245 -730

No files matched your search

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