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