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
+2789
-2274
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,45 +1,47 @@
|
|||||||
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 =
|
||||||
? getFovCone(userLocation, heading)
|
heading !== null && heading !== undefined
|
||||||
: null;
|
? getFovCone(userLocation, heading)
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MapView
|
<MapView
|
||||||
style={{flex: 1}}
|
style={{ flex: 1 }}
|
||||||
provider="google"
|
provider="google"
|
||||||
initialRegion={{...userLocation, latitudeDelta: 0.01, longitudeDelta: 0.01}}
|
initialRegion={{
|
||||||
scrollEnabled={true}
|
...userLocation,
|
||||||
zoomEnabled={true}
|
latitudeDelta: 0.01,
|
||||||
rotateEnabled={false}
|
longitudeDelta: 0.01,
|
||||||
pitchEnabled={false}
|
}}
|
||||||
showsUserLocation
|
scrollEnabled={true}
|
||||||
>
|
zoomEnabled={true}
|
||||||
{(visibleCaches || []).map((cache) => (
|
rotateEnabled={false}
|
||||||
<Marker
|
pitchEnabled={false}
|
||||||
key={cache.id}
|
showsUserLocation
|
||||||
coordinate={cache.coordinates}
|
>
|
||||||
pinColor="orange"
|
{(visibleCaches || []).map((cache) => (
|
||||||
title={cache.clue}
|
<Marker
|
||||||
/>
|
key={cache.id}
|
||||||
))}
|
coordinate={cache.coordinates}
|
||||||
{coneCoords && (
|
pinColor="orange"
|
||||||
<Polygon
|
title={cache.clue}
|
||||||
coordinates={coneCoords}
|
/>
|
||||||
fillColor="rgba(66,133,244,0.28)"
|
))}
|
||||||
strokeColor="rgba(66,133,244,0.50)"
|
{coneCoords && (
|
||||||
strokeWidth={1}
|
<Polygon
|
||||||
/>
|
coordinates={coneCoords}
|
||||||
)}
|
fillColor="rgba(66,133,244,0.28)"
|
||||||
</MapView>
|
strokeColor="rgba(66,133,244,0.50)"
|
||||||
);
|
strokeWidth={1}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</MapView>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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));
|
||||||
}
|
}
|
||||||
@@ -35,190 +44,208 @@ const DepartmentSettingsScreen = ({route, navigation}) => {
|
|||||||
load();
|
load();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
await updateSubgroup(SGid, {
|
await updateSubgroup(SGid, {
|
||||||
SubGroupName: deptName.trim(),
|
SubGroupName: deptName.trim(),
|
||||||
TeamsEnabled: teamsEnabled,
|
TeamsEnabled: teamsEnabled,
|
||||||
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.");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleManageCaches = () => {
|
|
||||||
navigation.navigate('MapScreen', {
|
|
||||||
selectedDepartmentSGid: SGid,
|
|
||||||
selectedDepartmentName: deptName || null,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// View -----------------------
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<Screen showBack={true} style={styles.center}>
|
|
||||||
<ActivityIndicator size="large"/>
|
|
||||||
</Screen>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Screen showBack={true}>
|
|
||||||
<ScrollView contentContainerStyle={styles.formContainer} showsVerticalScrollIndicator={false}>
|
|
||||||
<Text style={styles.sectionTitle}>Department Settings</Text>
|
|
||||||
|
|
||||||
<Text style={styles.label}>Department Name</Text>
|
|
||||||
<TextInput
|
|
||||||
style={styles.input}
|
|
||||||
value={deptName}
|
|
||||||
onChangeText={setDeptName}
|
|
||||||
placeholder="Enter department name"
|
|
||||||
placeholderTextColor="#9ca3af"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<View style={styles.codeSection}>
|
|
||||||
<Text style={styles.codeLabel}>Department Join Code</Text>
|
|
||||||
<Text style={styles.codeValue}>{joinCode || '—'}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.toggleRow}>
|
|
||||||
<Text style={styles.toggleLabel}>Teams Enabled</Text>
|
|
||||||
<Switch
|
|
||||||
value={teamsEnabled}
|
|
||||||
onValueChange={setTeamsEnabled}
|
|
||||||
trackColor={{false: '#d1d5db', true: '#93c5fd'}}
|
|
||||||
thumbColor={teamsEnabled ? '#2563eb' : '#9ca3af'}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Text style={styles.label}>Cache Claim Distance (metres)</Text>
|
|
||||||
<TextInput
|
|
||||||
style={styles.input}
|
|
||||||
value={cacheTriggerMeters}
|
|
||||||
onChangeText={setCacheTriggerMeters}
|
|
||||||
keyboardType="numeric"
|
|
||||||
placeholder="20"
|
|
||||||
placeholderTextColor="#9ca3af"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<View style={styles.infoRow}>
|
|
||||||
<Text style={styles.infoLabel}>Total Caches</Text>
|
|
||||||
<Text style={styles.infoValue}>{cacheCount}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.saveWrap}>
|
|
||||||
<ButtonTray>
|
|
||||||
<Button
|
|
||||||
label={saving ? 'Saving...' : 'Save Settings'}
|
|
||||||
onClick={handleSave}
|
|
||||||
styleButton={styles.saveButton}
|
|
||||||
styleLabel={styles.saveLabel}
|
|
||||||
/>
|
|
||||||
</ButtonTray>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.mapWrap}>
|
|
||||||
<ButtonTray>
|
|
||||||
<Button
|
|
||||||
label="Manage Caches"
|
|
||||||
onClick={handleManageCaches}
|
|
||||||
styleButton={styles.mapButton}
|
|
||||||
styleLabel={styles.mapLabel}
|
|
||||||
/>
|
|
||||||
</ButtonTray>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.resetWrap}>
|
|
||||||
<Button
|
|
||||||
label="Reset Department"
|
|
||||||
onClick={handleResetDepartment}
|
|
||||||
styleButton={styles.resetButton}
|
|
||||||
styleLabel={styles.resetLabel}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</ScrollView>
|
|
||||||
</Screen>
|
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleManageCaches = () => {
|
||||||
|
navigation.navigate("MapScreen", {
|
||||||
|
selectedDepartmentSGid: SGid,
|
||||||
|
selectedDepartmentName: deptName || null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// View -----------------------
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Screen showBack={true} style={styles.center}>
|
||||||
|
<ActivityIndicator size="large" />
|
||||||
|
</Screen>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Screen showBack={true}>
|
||||||
|
<ScrollView
|
||||||
|
contentContainerStyle={styles.formContainer}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
<Text style={styles.sectionTitle}>Department Settings</Text>
|
||||||
|
|
||||||
|
<Text style={styles.label}>Department Name</Text>
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
value={deptName}
|
||||||
|
onChangeText={setDeptName}
|
||||||
|
placeholder="Enter department name"
|
||||||
|
placeholderTextColor="#9ca3af"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<View style={styles.codeSection}>
|
||||||
|
<Text style={styles.codeLabel}>Department Join Code</Text>
|
||||||
|
<Text style={styles.codeValue}>{joinCode || "—"}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.toggleRow}>
|
||||||
|
<Text style={styles.toggleLabel}>Teams Enabled</Text>
|
||||||
|
<Switch
|
||||||
|
value={teamsEnabled}
|
||||||
|
onValueChange={setTeamsEnabled}
|
||||||
|
trackColor={{ false: "#d1d5db", true: "#93c5fd" }}
|
||||||
|
thumbColor={teamsEnabled ? "#2563eb" : "#9ca3af"}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={styles.label}>Cache Claim Distance (metres)</Text>
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
value={cacheTriggerMeters}
|
||||||
|
onChangeText={setCacheTriggerMeters}
|
||||||
|
keyboardType="numeric"
|
||||||
|
placeholder="20"
|
||||||
|
placeholderTextColor="#9ca3af"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<View style={styles.infoRow}>
|
||||||
|
<Text style={styles.infoLabel}>Total Caches</Text>
|
||||||
|
<Text style={styles.infoValue}>{cacheCount}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.saveWrap}>
|
||||||
|
<ButtonTray>
|
||||||
|
<Button
|
||||||
|
label={saving ? "Saving..." : "Save Settings"}
|
||||||
|
onClick={handleSave}
|
||||||
|
styleButton={styles.saveButton}
|
||||||
|
styleLabel={styles.saveLabel}
|
||||||
|
/>
|
||||||
|
</ButtonTray>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.mapWrap}>
|
||||||
|
<ButtonTray>
|
||||||
|
<Button
|
||||||
|
label="Manage Caches"
|
||||||
|
onClick={handleManageCaches}
|
||||||
|
styleButton={styles.mapButton}
|
||||||
|
styleLabel={styles.mapLabel}
|
||||||
|
/>
|
||||||
|
</ButtonTray>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.resetWrap}>
|
||||||
|
<Button
|
||||||
|
label="Reset Department"
|
||||||
|
onClick={handleResetDepartment}
|
||||||
|
styleButton={styles.resetButton}
|
||||||
|
styleLabel={styles.resetLabel}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</Screen>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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,
|
||||||
input: {
|
fontWeight: "700",
|
||||||
borderWidth: 1,
|
color: "#cdd6f4",
|
||||||
borderColor: '#45475a',
|
marginBottom: 16,
|
||||||
borderRadius: 8,
|
},
|
||||||
paddingHorizontal: 12,
|
label: {
|
||||||
paddingVertical: 10,
|
fontSize: 14,
|
||||||
fontSize: 16,
|
fontWeight: "600",
|
||||||
color: '#cdd6f4',
|
color: "#bac2de",
|
||||||
backgroundColor: '#313244',
|
marginBottom: 6,
|
||||||
},
|
marginTop: 14,
|
||||||
codeSection: {
|
},
|
||||||
flexDirection: 'row',
|
input: {
|
||||||
justifyContent: 'space-between',
|
borderWidth: 1,
|
||||||
alignItems: 'center',
|
borderColor: "#45475a",
|
||||||
marginTop: 16,
|
borderRadius: 8,
|
||||||
paddingVertical: 12,
|
paddingHorizontal: 12,
|
||||||
paddingHorizontal: 12,
|
paddingVertical: 10,
|
||||||
backgroundColor: '#313244',
|
fontSize: 16,
|
||||||
borderRadius: 8,
|
color: "#cdd6f4",
|
||||||
},
|
backgroundColor: "#313244",
|
||||||
codeLabel: {fontSize: 14, fontWeight: '600', color: '#bac2de'},
|
},
|
||||||
codeValue: {fontSize: 16, fontWeight: '700', color: '#bd93f9', letterSpacing: 2},
|
codeSection: {
|
||||||
toggleRow: {
|
flexDirection: "row",
|
||||||
flexDirection: 'row',
|
justifyContent: "space-between",
|
||||||
justifyContent: 'space-between',
|
alignItems: "center",
|
||||||
alignItems: 'center',
|
marginTop: 16,
|
||||||
marginTop: 16,
|
paddingVertical: 12,
|
||||||
paddingVertical: 10,
|
paddingHorizontal: 12,
|
||||||
borderBottomWidth: 1,
|
backgroundColor: "#313244",
|
||||||
borderBottomColor: '#45475a',
|
borderRadius: 8,
|
||||||
},
|
},
|
||||||
toggleLabel: {fontSize: 14, fontWeight: '600', color: '#bac2de'},
|
codeLabel: { fontSize: 14, fontWeight: "600", color: "#bac2de" },
|
||||||
infoRow: {
|
codeValue: {
|
||||||
flexDirection: 'row',
|
fontSize: 16,
|
||||||
justifyContent: 'space-between',
|
fontWeight: "700",
|
||||||
alignItems: 'center',
|
color: "#bd93f9",
|
||||||
marginTop: 16,
|
letterSpacing: 2,
|
||||||
paddingVertical: 12,
|
},
|
||||||
paddingHorizontal: 12,
|
toggleRow: {
|
||||||
backgroundColor: '#313244',
|
flexDirection: "row",
|
||||||
borderRadius: 8,
|
justifyContent: "space-between",
|
||||||
},
|
alignItems: "center",
|
||||||
infoLabel: {fontSize: 14, fontWeight: '600', color: '#bac2de'},
|
marginTop: 16,
|
||||||
infoValue: {fontSize: 16, fontWeight: '700', color: '#cdd6f4'},
|
paddingVertical: 10,
|
||||||
saveWrap: {marginTop: 24},
|
borderBottomWidth: 1,
|
||||||
saveButton: {backgroundColor: '#a6e3a1', borderColor: '#a6e3a1'},
|
borderBottomColor: "#45475a",
|
||||||
saveLabel: {color: '#1e1e2e', fontWeight: '600'},
|
},
|
||||||
mapWrap: {marginTop: 16},
|
toggleLabel: { fontSize: 14, fontWeight: "600", color: "#bac2de" },
|
||||||
mapButton: {backgroundColor: '#bd93f9', borderColor: '#bd93f9'},
|
infoRow: {
|
||||||
mapLabel: {color: '#1e1e2e', fontWeight: '600'},
|
flexDirection: "row",
|
||||||
resetWrap: {marginTop: 30},
|
justifyContent: "space-between",
|
||||||
resetButton: {backgroundColor: '#86efac', borderColor: '#86efac'},
|
alignItems: "center",
|
||||||
resetLabel: {color: '#1e1e2e', fontWeight: '600'},
|
marginTop: 16,
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
backgroundColor: "#313244",
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
infoLabel: { fontSize: 14, fontWeight: "600", color: "#bac2de" },
|
||||||
|
infoValue: { fontSize: 16, fontWeight: "700", color: "#cdd6f4" },
|
||||||
|
saveWrap: { marginTop: 24 },
|
||||||
|
saveButton: { backgroundColor: "#a6e3a1", borderColor: "#a6e3a1" },
|
||||||
|
saveLabel: { color: "#1e1e2e", fontWeight: "600" },
|
||||||
|
mapWrap: { marginTop: 16 },
|
||||||
|
mapButton: { backgroundColor: "#bd93f9", borderColor: "#bd93f9" },
|
||||||
|
mapLabel: { color: "#1e1e2e", fontWeight: "600" },
|
||||||
|
resetWrap: { marginTop: 30 },
|
||||||
|
resetButton: { backgroundColor: "#86efac", borderColor: "#86efac" },
|
||||||
|
resetLabel: { color: "#1e1e2e", fontWeight: "600" },
|
||||||
});
|
});
|
||||||
|
|
||||||
export default DepartmentSettingsScreen;
|
export default DepartmentSettingsScreen;
|
||||||
|
|
||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -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,411 +1,525 @@
|
|||||||
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 {
|
||||||
const isBusiness = Boolean(session.isBusiness);
|
getTeams,
|
||||||
const isAdmin = Boolean(session.isAcceptedAdmin);
|
getTeamMembers,
|
||||||
const selectedDepartment = route?.params?.department || null;
|
getGroupMembers,
|
||||||
const effectiveSGid = isBusiness
|
removeMember,
|
||||||
? ((isAdmin && selectedDepartment?.SGid) ? selectedDepartment.SGid : (isAdmin ? null : session.currentSGid))
|
resetPlayerProgress,
|
||||||
: null;
|
getUser,
|
||||||
|
getAdminWaitlist,
|
||||||
|
approveAdmin,
|
||||||
|
rejectAdmin,
|
||||||
|
getSubgroups,
|
||||||
|
} = useGameHook();
|
||||||
|
const isBusiness = Boolean(session.isBusiness);
|
||||||
|
const isAdmin = Boolean(session.isAcceptedAdmin);
|
||||||
|
const selectedDepartment = route?.params?.department || null;
|
||||||
|
const effectiveSGid = isBusiness
|
||||||
|
? isAdmin && selectedDepartment?.SGid
|
||||||
|
? selectedDepartment.SGid
|
||||||
|
: isAdmin
|
||||||
|
? null
|
||||||
|
: session.currentSGid
|
||||||
|
: null;
|
||||||
|
|
||||||
// State ----------------------
|
// State ----------------------
|
||||||
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [players, setPlayers] = useState([]);
|
const [players, setPlayers] = useState([]);
|
||||||
const [departmentList, setDepartmentList] = useState([]);
|
const [departmentList, setDepartmentList] = useState([]);
|
||||||
const [adminTeam, setAdminTeam] = useState([]);
|
const [adminTeam, setAdminTeam] = useState([]);
|
||||||
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 () => {
|
|
||||||
if (!session.currentGid) { setLoading(false); return; }
|
|
||||||
|
|
||||||
if (isBusiness && isAdmin && !selectedDepartment) {
|
|
||||||
const sgs = await getSubgroups(session.currentGid);
|
|
||||||
setDepartmentList((sgs || []).filter((sg) => !sg.IsAdminGroup));
|
|
||||||
|
|
||||||
const allOrgMembers = await getGroupMembers(session.currentGid, null);
|
|
||||||
const seenAdminUids = new Set();
|
|
||||||
const acceptedAdmins = (allOrgMembers || [])
|
|
||||||
.filter((m) => m.IsAcceptedAdmin)
|
|
||||||
.sort((a, b) => Number(a.id) - Number(b.id))
|
|
||||||
.filter((m) => {
|
|
||||||
if (seenAdminUids.has(m.Uid)) return false;
|
|
||||||
seenAdminUids.add(m.Uid);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
const adminRows = [];
|
|
||||||
for (const member of acceptedAdmins) {
|
|
||||||
const user = await getUser(member.Uid);
|
|
||||||
if (!user) continue;
|
|
||||||
adminRows.push({
|
|
||||||
Uid: member.Uid,
|
|
||||||
username: user.username || `User ${member.Uid}`,
|
|
||||||
membershipId: member.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setAdminTeam(adminRows);
|
|
||||||
setIsCurrentUserAdminLead(adminRows.length > 0 && adminRows[0].Uid === session.currentUid);
|
|
||||||
setPlayers([]);
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setAdminTeam([]);
|
|
||||||
setIsCurrentUserAdminLead(false);
|
|
||||||
|
|
||||||
// Load subgroup names for department display (business accounts)
|
|
||||||
let subgroupNameMap = {};
|
|
||||||
if (isBusiness) {
|
|
||||||
const sgs = await getSubgroups(session.currentGid);
|
|
||||||
for (const sg of (sgs || [])) {
|
|
||||||
if (!sg.IsAdminGroup) subgroupNameMap[sg.SGid] = sg.SubGroupName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get ALL members (including admins)
|
|
||||||
const allMembers = await getGroupMembers(session.currentGid, effectiveSGid ?? null);
|
|
||||||
|
|
||||||
// Get admin waitlist entries
|
|
||||||
const waitlist = effectiveSGid ? [] : await getAdminWaitlist(session.currentGid);
|
|
||||||
|
|
||||||
// Deduplicate allMembers by Uid — a user may have two rows if they were a
|
|
||||||
// regular member before being approved as an admin. Prefer the admin row.
|
|
||||||
const seenUids = new Set();
|
|
||||||
const uniqueMembers = (allMembers || [])
|
|
||||||
.sort((a, b) => (b.IsAcceptedAdmin ? 1 : 0) - (a.IsAcceptedAdmin ? 1 : 0))
|
|
||||||
.filter((m) => { if (seenUids.has(m.Uid)) return false; seenUids.add(m.Uid); return true; });
|
|
||||||
|
|
||||||
// Build team lookup: { Uid -> { teamCode, teamName, isLeader } }
|
|
||||||
const allTeams = await getTeams(session.currentGid, effectiveSGid ?? null);
|
|
||||||
const teamInfoByUid = {};
|
|
||||||
for (const team of (allTeams || [])) {
|
|
||||||
const tms = await getTeamMembers(team.Tid);
|
|
||||||
for (const tm of (tms || [])) {
|
|
||||||
teamInfoByUid[tm.Uid] = {
|
|
||||||
teamCode: team.JoinCode,
|
|
||||||
teamName: team.TeamName,
|
|
||||||
isLeader: Boolean(tm.IsLeader),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build player list — include admins with tags
|
|
||||||
const playerList = [];
|
|
||||||
for (const m of uniqueMembers) {
|
|
||||||
// Skip the current admin viewing the list
|
|
||||||
if (m.Uid === session.currentUid) continue;
|
|
||||||
const user = await getUser(m.Uid);
|
|
||||||
if (user) {
|
|
||||||
// A group member may also be on the admin waitlist (applied after joining as a player).
|
|
||||||
// Check the waitlist first so they are treated as pending, not a regular player.
|
|
||||||
const waitlistEntry = (waitlist || []).find((w) => w.Uid === m.Uid);
|
|
||||||
let adminTag = null;
|
|
||||||
let waitlistId = null;
|
|
||||||
if (waitlistEntry) {
|
|
||||||
adminTag = '[Admin Awaiting Response]';
|
|
||||||
waitlistId = waitlistEntry.id;
|
|
||||||
} else if (m.IsAcceptedAdmin) {
|
|
||||||
adminTag = '[ADMIN]';
|
|
||||||
}
|
|
||||||
playerList.push({
|
|
||||||
...user,
|
|
||||||
membershipId: m.id,
|
|
||||||
team: teamInfoByUid[m.Uid] || null,
|
|
||||||
adminTag,
|
|
||||||
waitlistId,
|
|
||||||
departmentName: subgroupNameMap[m.SGid] || null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Include waitlisted users who are not yet group members at all
|
|
||||||
for (const w of (waitlist || [])) {
|
|
||||||
if (playerList.some((p) => p.Uid === w.Uid)) continue;
|
|
||||||
if (w.Uid === session.currentUid) continue;
|
|
||||||
const user = await getUser(w.Uid);
|
|
||||||
if (user) {
|
|
||||||
playerList.push({
|
|
||||||
...user,
|
|
||||||
membershipId: null,
|
|
||||||
team: null,
|
|
||||||
adminTag: '[Admin Awaiting Response]',
|
|
||||||
waitlistId: w.id,
|
|
||||||
departmentName: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setPlayers(playerList);
|
|
||||||
setLoading(false);
|
|
||||||
}, [session.currentGid, session.currentUid, session.currentSGid, isBusiness, isAdmin, selectedDepartment, selectedDepartment?.SGid, effectiveSGid]);
|
|
||||||
|
|
||||||
useEffect(() => { loadData(); }, [loadData]);
|
|
||||||
|
|
||||||
const handleRemovePlayer = async (membershipId) => {
|
|
||||||
await removeMember(membershipId);
|
|
||||||
await loadData();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleResetPlayerProgress = (player) => {
|
|
||||||
Alert.alert(
|
|
||||||
'Reset Progress',
|
|
||||||
`Reset all cache claims for ${player.username}?`,
|
|
||||||
[
|
|
||||||
{text: 'Cancel', style: 'cancel'},
|
|
||||||
{
|
|
||||||
text: 'Reset',
|
|
||||||
style: 'destructive',
|
|
||||||
onPress: async () => {
|
|
||||||
await resetPlayerProgress(session.currentGid, player.Uid, effectiveSGid);
|
|
||||||
Alert.alert('Done', `Progress for ${player.username} has been reset.`);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Approve a waitlisted admin and refresh the list
|
|
||||||
const handleApproveAdmin = async (player) => {
|
|
||||||
await approveAdmin(player.waitlistId);
|
|
||||||
await loadData();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Deny (remove from waitlist) a pending admin and refresh the list
|
|
||||||
const handleDenyAdmin = async (player) => {
|
|
||||||
await rejectAdmin(player.waitlistId);
|
|
||||||
await loadData();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKickAdmin = (adminMember) => {
|
|
||||||
if (!isCurrentUserAdminLead || adminMember.Uid === session.currentUid) return;
|
|
||||||
Alert.alert(
|
|
||||||
'Kick Admin',
|
|
||||||
`Remove ${adminMember.username} from the admin team?`,
|
|
||||||
[
|
|
||||||
{text: 'Cancel', style: 'cancel'},
|
|
||||||
{
|
|
||||||
text: 'Kick',
|
|
||||||
style: 'destructive',
|
|
||||||
onPress: async () => {
|
|
||||||
await removeMember(adminMember.membershipId);
|
|
||||||
await loadData();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// View -----------------------
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<Screen style={styles.center}>
|
|
||||||
<ActivityIndicator size="large"/>
|
|
||||||
</Screen>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const loadData = useCallback(async () => {
|
||||||
if (!session.currentGid) {
|
if (!session.currentGid) {
|
||||||
return (
|
setLoading(false);
|
||||||
<Screen style={styles.center}>
|
return;
|
||||||
<Text style={styles.body}>Create or join a game first.</Text>
|
|
||||||
</Screen>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (session.isPendingAdmin && !session.isAcceptedAdmin) {
|
|
||||||
return <PendingApprovalView/>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isBusiness && isAdmin && !selectedDepartment) {
|
if (isBusiness && isAdmin && !selectedDepartment) {
|
||||||
return (
|
const sgs = await getSubgroups(session.currentGid);
|
||||||
<Screen style={styles.container}>
|
setDepartmentList((sgs || []).filter((sg) => !sg.IsAdminGroup));
|
||||||
<Text style={styles.sectionTitle}>Departments</Text>
|
|
||||||
<ScrollView style={styles.listSection}>
|
const allOrgMembers = await getGroupMembers(session.currentGid, null);
|
||||||
<Card>
|
const seenAdminUids = new Set();
|
||||||
<Pressable onPress={() => setAdminTeamExpanded((prev) => !prev)}>
|
const acceptedAdmins = (allOrgMembers || [])
|
||||||
<View style={styles.playerRow}>
|
.filter((m) => m.IsAcceptedAdmin)
|
||||||
<Text style={styles.playerName}>Admin Team</Text>
|
.sort((a, b) => Number(a.id) - Number(b.id))
|
||||||
<Text style={styles.openLabel}>{adminTeamExpanded ? 'Close' : 'Open'}</Text>
|
.filter((m) => {
|
||||||
</View>
|
if (seenAdminUids.has(m.Uid)) return false;
|
||||||
</Pressable>
|
seenAdminUids.add(m.Uid);
|
||||||
{adminTeamExpanded && (
|
return true;
|
||||||
<View style={styles.adminTeamWrap}>
|
});
|
||||||
{adminTeam.map((adminMember, index) => (
|
|
||||||
<View key={adminMember.Uid} style={styles.adminTeamRow}>
|
const adminRows = [];
|
||||||
<Text style={styles.adminTeamName} numberOfLines={1}>
|
for (const member of acceptedAdmins) {
|
||||||
{adminMember.username}{index === 0 ? ' (Admin Lead)' : ''}
|
const user = await getUser(member.Uid);
|
||||||
</Text>
|
if (!user) continue;
|
||||||
{isCurrentUserAdminLead && adminMember.Uid !== session.currentUid && (
|
adminRows.push({
|
||||||
<Button
|
Uid: member.Uid,
|
||||||
label="Kick Admin"
|
username: user.username || `User ${member.Uid}`,
|
||||||
onClick={() => handleKickAdmin(adminMember)}
|
membershipId: member.id,
|
||||||
styleButton={styles.removeButton}
|
});
|
||||||
styleLabel={styles.kickLabel}
|
}
|
||||||
/>
|
setAdminTeam(adminRows);
|
||||||
)}
|
setIsCurrentUserAdminLead(
|
||||||
</View>
|
adminRows.length > 0 && adminRows[0].Uid === session.currentUid,
|
||||||
))}
|
);
|
||||||
{adminTeam.length === 0 && (
|
setPlayers([]);
|
||||||
<Text style={styles.emptyText}>No approved admins yet.</Text>
|
setLoading(false);
|
||||||
)}
|
return;
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
{departmentList.map((department) => (
|
|
||||||
<Pressable
|
|
||||||
key={department.SGid}
|
|
||||||
onPress={() => navigation.navigate('PlayersScreen', {department})}
|
|
||||||
>
|
|
||||||
<Card>
|
|
||||||
<View style={styles.playerRow}>
|
|
||||||
<Text style={styles.playerName}>{department.SubGroupName}</Text>
|
|
||||||
<Text style={styles.openLabel}>Open</Text>
|
|
||||||
</View>
|
|
||||||
</Card>
|
|
||||||
</Pressable>
|
|
||||||
))}
|
|
||||||
{departmentList.length === 0 && (
|
|
||||||
<Text style={styles.emptyText}>No departments yet.</Text>
|
|
||||||
)}
|
|
||||||
</ScrollView>
|
|
||||||
</Screen>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
setAdminTeam([]);
|
||||||
<Screen style={styles.container} showBack={Boolean(selectedDepartment)}>
|
setIsCurrentUserAdminLead(false);
|
||||||
<Text style={styles.sectionTitle}>Players</Text>
|
|
||||||
<ScrollView style={styles.listSection}>
|
// Load subgroup names for department display (business accounts)
|
||||||
{players.map((player) => (
|
let subgroupNameMap = {};
|
||||||
// Waitlisted admins get a light red tint so they stand out from regular players
|
if (isBusiness) {
|
||||||
<Card key={player.Uid} style={player.waitlistId ? styles.waitlistCard : undefined}>
|
const sgs = await getSubgroups(session.currentGid);
|
||||||
<View style={styles.playerRow}>
|
for (const sg of sgs || []) {
|
||||||
<Text style={styles.playerName} numberOfLines={1}>
|
if (!sg.IsAdminGroup) subgroupNameMap[sg.SGid] = sg.SubGroupName;
|
||||||
{player.username}
|
}
|
||||||
{player.adminTag && (
|
}
|
||||||
<Text style={player.adminTag === '[ADMIN]' ? styles.adminBadge : styles.waitlistBadge}>
|
|
||||||
{' '}{player.adminTag}
|
// Get ALL members (including admins)
|
||||||
</Text>
|
const allMembers = await getGroupMembers(
|
||||||
)}
|
session.currentGid,
|
||||||
{/* Show department name in brackets for non-admin players (org accounts) */}
|
effectiveSGid ?? null,
|
||||||
{!player.adminTag && player.departmentName && (
|
|
||||||
<Text style={styles.departmentName}>
|
|
||||||
{' '}({player.departmentName})
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
<View style={styles.playerMeta}>
|
|
||||||
{player.team?.isLeader && (
|
|
||||||
<Text style={styles.leaderBadge}>(Team Leader)</Text>
|
|
||||||
)}
|
|
||||||
{player.team && (
|
|
||||||
<Text style={styles.teamCodeBadge}>{player.team.teamCode}</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<View style={styles.actionRow}>
|
|
||||||
{/* Waitlisted admins get Approve / Deny instead of the normal player actions */}
|
|
||||||
{player.waitlistId ? (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
label="Approve"
|
|
||||||
onClick={() => handleApproveAdmin(player)}
|
|
||||||
styleButton={styles.approveButton}
|
|
||||||
styleLabel={styles.actionLabel}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
label="Deny"
|
|
||||||
onClick={() => handleDenyAdmin(player)}
|
|
||||||
styleButton={styles.denyButton}
|
|
||||||
styleLabel={styles.actionLabel}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{!player.adminTag && (
|
|
||||||
<Button
|
|
||||||
label="Reset Progress"
|
|
||||||
onClick={() => handleResetPlayerProgress(player)}
|
|
||||||
styleButton={styles.resetButton}
|
|
||||||
styleLabel={styles.actionLabel}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{player.membershipId && (
|
|
||||||
<Button
|
|
||||||
label="Kick"
|
|
||||||
onClick={() => handleRemovePlayer(player.membershipId)}
|
|
||||||
styleButton={styles.removeButton}
|
|
||||||
styleLabel={styles.kickLabel}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
{players.length === 0 && (
|
|
||||||
<Text style={styles.emptyText}>No players have joined yet.</Text>
|
|
||||||
)}
|
|
||||||
</ScrollView>
|
|
||||||
</Screen>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Get admin waitlist entries
|
||||||
|
const waitlist = effectiveSGid
|
||||||
|
? []
|
||||||
|
: await getAdminWaitlist(session.currentGid);
|
||||||
|
|
||||||
|
// Deduplicate allMembers by Uid — a user may have two rows if they were a
|
||||||
|
// regular member before being approved as an admin. Prefer the admin row.
|
||||||
|
const seenUids = new Set();
|
||||||
|
const uniqueMembers = (allMembers || [])
|
||||||
|
.sort((a, b) => (b.IsAcceptedAdmin ? 1 : 0) - (a.IsAcceptedAdmin ? 1 : 0))
|
||||||
|
.filter((m) => {
|
||||||
|
if (seenUids.has(m.Uid)) return false;
|
||||||
|
seenUids.add(m.Uid);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build team lookup: { Uid -> { teamCode, teamName, isLeader } }
|
||||||
|
const allTeams = await getTeams(session.currentGid, effectiveSGid ?? null);
|
||||||
|
const teamInfoByUid = {};
|
||||||
|
for (const team of allTeams || []) {
|
||||||
|
const tms = await getTeamMembers(team.Tid);
|
||||||
|
for (const tm of tms || []) {
|
||||||
|
teamInfoByUid[tm.Uid] = {
|
||||||
|
teamCode: team.JoinCode,
|
||||||
|
teamName: team.TeamName,
|
||||||
|
isLeader: Boolean(tm.IsLeader),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build player list — include admins with tags
|
||||||
|
const playerList = [];
|
||||||
|
for (const m of uniqueMembers) {
|
||||||
|
// Skip the current admin viewing the list
|
||||||
|
if (m.Uid === session.currentUid) continue;
|
||||||
|
const user = await getUser(m.Uid);
|
||||||
|
if (user) {
|
||||||
|
// A group member may also be on the admin waitlist (applied after joining as a player).
|
||||||
|
// Check the waitlist first so they are treated as pending, not a regular player.
|
||||||
|
const waitlistEntry = (waitlist || []).find((w) => w.Uid === m.Uid);
|
||||||
|
let adminTag = null;
|
||||||
|
let waitlistId = null;
|
||||||
|
if (waitlistEntry) {
|
||||||
|
adminTag = "[Admin Awaiting Response]";
|
||||||
|
waitlistId = waitlistEntry.id;
|
||||||
|
} else if (m.IsAcceptedAdmin) {
|
||||||
|
adminTag = "[ADMIN]";
|
||||||
|
}
|
||||||
|
playerList.push({
|
||||||
|
...user,
|
||||||
|
membershipId: m.id,
|
||||||
|
team: teamInfoByUid[m.Uid] || null,
|
||||||
|
adminTag,
|
||||||
|
waitlistId,
|
||||||
|
departmentName: subgroupNameMap[m.SGid] || null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include waitlisted users who are not yet group members at all
|
||||||
|
for (const w of waitlist || []) {
|
||||||
|
if (playerList.some((p) => p.Uid === w.Uid)) continue;
|
||||||
|
if (w.Uid === session.currentUid) continue;
|
||||||
|
const user = await getUser(w.Uid);
|
||||||
|
if (user) {
|
||||||
|
playerList.push({
|
||||||
|
...user,
|
||||||
|
membershipId: null,
|
||||||
|
team: null,
|
||||||
|
adminTag: "[Admin Awaiting Response]",
|
||||||
|
waitlistId: w.id,
|
||||||
|
departmentName: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setPlayers(playerList);
|
||||||
|
setLoading(false);
|
||||||
|
}, [
|
||||||
|
session.currentGid,
|
||||||
|
session.currentUid,
|
||||||
|
session.currentSGid,
|
||||||
|
isBusiness,
|
||||||
|
isAdmin,
|
||||||
|
selectedDepartment,
|
||||||
|
selectedDepartment?.SGid,
|
||||||
|
effectiveSGid,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadData();
|
||||||
|
}, [loadData]);
|
||||||
|
|
||||||
|
const handleRemovePlayer = async (membershipId) => {
|
||||||
|
await removeMember(membershipId);
|
||||||
|
await loadData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetPlayerProgress = (player) => {
|
||||||
|
Alert.alert(
|
||||||
|
"Reset Progress",
|
||||||
|
`Reset all cache claims for ${player.username}?`,
|
||||||
|
[
|
||||||
|
{ text: "Cancel", style: "cancel" },
|
||||||
|
{
|
||||||
|
text: "Reset",
|
||||||
|
style: "destructive",
|
||||||
|
onPress: async () => {
|
||||||
|
await resetPlayerProgress(
|
||||||
|
session.currentGid,
|
||||||
|
player.Uid,
|
||||||
|
effectiveSGid,
|
||||||
|
);
|
||||||
|
Alert.alert(
|
||||||
|
"Done",
|
||||||
|
`Progress for ${player.username} has been reset.`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Approve a waitlisted admin and refresh the list
|
||||||
|
const handleApproveAdmin = async (player) => {
|
||||||
|
await approveAdmin(player.waitlistId);
|
||||||
|
await loadData();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Deny (remove from waitlist) a pending admin and refresh the list
|
||||||
|
const handleDenyAdmin = async (player) => {
|
||||||
|
await rejectAdmin(player.waitlistId);
|
||||||
|
await loadData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKickAdmin = (adminMember) => {
|
||||||
|
if (!isCurrentUserAdminLead || adminMember.Uid === session.currentUid)
|
||||||
|
return;
|
||||||
|
Alert.alert(
|
||||||
|
"Kick Admin",
|
||||||
|
`Remove ${adminMember.username} from the admin team?`,
|
||||||
|
[
|
||||||
|
{ text: "Cancel", style: "cancel" },
|
||||||
|
{
|
||||||
|
text: "Kick",
|
||||||
|
style: "destructive",
|
||||||
|
onPress: async () => {
|
||||||
|
await removeMember(adminMember.membershipId);
|
||||||
|
await loadData();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// View -----------------------
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Screen style={styles.center}>
|
||||||
|
<ActivityIndicator size="large" />
|
||||||
|
</Screen>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!session.currentGid) {
|
||||||
|
return (
|
||||||
|
<Screen style={styles.center}>
|
||||||
|
<Text style={styles.body}>Create or join a game first.</Text>
|
||||||
|
</Screen>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.isPendingAdmin && !session.isAcceptedAdmin) {
|
||||||
|
return <PendingApprovalView />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBusiness && isAdmin && !selectedDepartment) {
|
||||||
|
return (
|
||||||
|
<Screen style={styles.container}>
|
||||||
|
<Text style={styles.sectionTitle}>Departments</Text>
|
||||||
|
<ScrollView style={styles.listSection}>
|
||||||
|
<Card>
|
||||||
|
<Pressable onPress={() => setAdminTeamExpanded((prev) => !prev)}>
|
||||||
|
<View style={styles.playerRow}>
|
||||||
|
<Text style={styles.playerName}>Admin Team</Text>
|
||||||
|
<Text style={styles.openLabel}>
|
||||||
|
{adminTeamExpanded ? "Close" : "Open"}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
{adminTeamExpanded && (
|
||||||
|
<View style={styles.adminTeamWrap}>
|
||||||
|
{adminTeam.map((adminMember, index) => (
|
||||||
|
<View key={adminMember.Uid} style={styles.adminTeamRow}>
|
||||||
|
<Text style={styles.adminTeamName} numberOfLines={1}>
|
||||||
|
{adminMember.username}
|
||||||
|
{index === 0 ? " (Admin Lead)" : ""}
|
||||||
|
</Text>
|
||||||
|
{isCurrentUserAdminLead &&
|
||||||
|
adminMember.Uid !== session.currentUid && (
|
||||||
|
<Button
|
||||||
|
label="Kick Admin"
|
||||||
|
onClick={() => handleKickAdmin(adminMember)}
|
||||||
|
styleButton={styles.removeButton}
|
||||||
|
styleLabel={styles.kickLabel}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
{adminTeam.length === 0 && (
|
||||||
|
<Text style={styles.emptyText}>No approved admins yet.</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
{departmentList.map((department) => (
|
||||||
|
<Pressable
|
||||||
|
key={department.SGid}
|
||||||
|
onPress={() =>
|
||||||
|
navigation.navigate("PlayersScreen", { department })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Card>
|
||||||
|
<View style={styles.playerRow}>
|
||||||
|
<Text style={styles.playerName}>
|
||||||
|
{department.SubGroupName}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.openLabel}>Open</Text>
|
||||||
|
</View>
|
||||||
|
</Card>
|
||||||
|
</Pressable>
|
||||||
|
))}
|
||||||
|
{departmentList.length === 0 && (
|
||||||
|
<Text style={styles.emptyText}>No departments yet.</Text>
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
</Screen>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Screen style={styles.container} showBack={Boolean(selectedDepartment)}>
|
||||||
|
<Text style={styles.sectionTitle}>Players</Text>
|
||||||
|
<ScrollView style={styles.listSection}>
|
||||||
|
{players.map((player) => (
|
||||||
|
// Waitlisted admins get a light red tint so they stand out from regular players
|
||||||
|
<Card
|
||||||
|
key={player.Uid}
|
||||||
|
style={player.waitlistId ? styles.waitlistCard : undefined}
|
||||||
|
>
|
||||||
|
<View style={styles.playerRow}>
|
||||||
|
<Text style={styles.playerName} numberOfLines={1}>
|
||||||
|
{player.username}
|
||||||
|
{player.adminTag && (
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
player.adminTag === "[ADMIN]"
|
||||||
|
? styles.adminBadge
|
||||||
|
: styles.waitlistBadge
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
{player.adminTag}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{/* Show department name in brackets for non-admin players (org accounts) */}
|
||||||
|
{!player.adminTag && player.departmentName && (
|
||||||
|
<Text style={styles.departmentName}>
|
||||||
|
{" "}
|
||||||
|
({player.departmentName})
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
<View style={styles.playerMeta}>
|
||||||
|
{player.team?.isLeader && (
|
||||||
|
<Text style={styles.leaderBadge}>(Team Leader)</Text>
|
||||||
|
)}
|
||||||
|
{player.team && (
|
||||||
|
<Text style={styles.teamCodeBadge}>
|
||||||
|
{player.team.teamCode}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={styles.actionRow}>
|
||||||
|
{/* Waitlisted admins get Approve / Deny instead of the normal player actions */}
|
||||||
|
{player.waitlistId ? (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
label="Approve"
|
||||||
|
onClick={() => handleApproveAdmin(player)}
|
||||||
|
styleButton={styles.approveButton}
|
||||||
|
styleLabel={styles.actionLabel}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="Deny"
|
||||||
|
onClick={() => handleDenyAdmin(player)}
|
||||||
|
styleButton={styles.denyButton}
|
||||||
|
styleLabel={styles.actionLabel}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{!player.adminTag && (
|
||||||
|
<Button
|
||||||
|
label="Reset Progress"
|
||||||
|
onClick={() => handleResetPlayerProgress(player)}
|
||||||
|
styleButton={styles.resetButton}
|
||||||
|
styleLabel={styles.actionLabel}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{player.membershipId && (
|
||||||
|
<Button
|
||||||
|
label="Kick"
|
||||||
|
onClick={() => handleRemovePlayer(player.membershipId)}
|
||||||
|
styleButton={styles.removeButton}
|
||||||
|
styleLabel={styles.kickLabel}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
{players.length === 0 && (
|
||||||
|
<Text style={styles.emptyText}>No players have joined yet.</Text>
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
</Screen>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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: {
|
||||||
playerRow: {
|
color: "#6c7086",
|
||||||
flexDirection: 'row',
|
textAlign: "center",
|
||||||
alignItems: 'center',
|
marginTop: 30,
|
||||||
gap: 8,
|
fontSize: 14,
|
||||||
marginBottom: 8,
|
},
|
||||||
},
|
playerRow: {
|
||||||
playerName: {fontSize: 15, fontWeight: '600', color: '#cdd6f4', flex: 1},
|
flexDirection: "row",
|
||||||
playerMeta: {flexDirection: 'row', alignItems: 'center', gap: 6},
|
alignItems: "center",
|
||||||
leaderBadge: {fontSize: 11, color: '#bd93f9', fontWeight: '700'},
|
gap: 8,
|
||||||
teamCodeBadge: {fontSize: 12, fontWeight: '700', color: '#a6e3a1', letterSpacing: 1},
|
marginBottom: 8,
|
||||||
adminBadge: {fontSize: 12, fontWeight: '700', color: '#D92800'},
|
},
|
||||||
waitlistBadge: {fontSize: 11, fontWeight: '600', color: '#f4a460'},
|
playerName: { fontSize: 15, fontWeight: "600", color: "#cdd6f4", flex: 1 },
|
||||||
actionRow: {flexDirection: 'row', gap: 8},
|
playerMeta: { flexDirection: "row", alignItems: "center", gap: 6 },
|
||||||
resetButton: {backgroundColor: '#86efac', borderColor: '#86efac', minHeight: 36, flex: 1, paddingHorizontal: 10},
|
leaderBadge: { fontSize: 11, color: "#bd93f9", fontWeight: "700" },
|
||||||
removeButton: {backgroundColor: '#D92800', borderColor: '#D92800', minHeight: 36, flex: 1, paddingHorizontal: 10},
|
teamCodeBadge: {
|
||||||
actionLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 13},
|
fontSize: 12,
|
||||||
kickLabel: {color: '#ffffff', fontWeight: '600', fontSize: 13},
|
fontWeight: "700",
|
||||||
adminTeamWrap: {gap: 8},
|
color: "#a6e3a1",
|
||||||
adminTeamRow: {flexDirection: 'row', alignItems: 'center', gap: 8},
|
letterSpacing: 1,
|
||||||
adminTeamName: {flex: 1, fontSize: 14, color: '#cdd6f4', fontWeight: '600'},
|
},
|
||||||
departmentName: {fontSize: 14, color: '#6c7086', fontStyle: 'italic'},
|
adminBadge: { fontSize: 12, fontWeight: "700", color: "#D92800" },
|
||||||
waitlistCard: {backgroundColor: 'rgba(217, 40, 0, 0.15)', borderColor: '#D92800'},
|
waitlistBadge: { fontSize: 11, fontWeight: "600", color: "#f4a460" },
|
||||||
approveButton: {backgroundColor: '#a6e3a1', borderColor: '#a6e3a1', minHeight: 36, flex: 1, paddingHorizontal: 10},
|
actionRow: { flexDirection: "row", gap: 8 },
|
||||||
denyButton: {backgroundColor: '#D92800', borderColor: '#D92800', minHeight: 36, flex: 1, paddingHorizontal: 10},
|
resetButton: {
|
||||||
|
backgroundColor: "#86efac",
|
||||||
|
borderColor: "#86efac",
|
||||||
|
minHeight: 36,
|
||||||
|
flex: 1,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
},
|
||||||
|
removeButton: {
|
||||||
|
backgroundColor: "#D92800",
|
||||||
|
borderColor: "#D92800",
|
||||||
|
minHeight: 36,
|
||||||
|
flex: 1,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
},
|
||||||
|
actionLabel: { color: "#1e1e2e", fontWeight: "600", fontSize: 13 },
|
||||||
|
kickLabel: { color: "#ffffff", fontWeight: "600", fontSize: 13 },
|
||||||
|
adminTeamWrap: { gap: 8 },
|
||||||
|
adminTeamRow: { flexDirection: "row", alignItems: "center", gap: 8 },
|
||||||
|
adminTeamName: { flex: 1, fontSize: 14, color: "#cdd6f4", fontWeight: "600" },
|
||||||
|
departmentName: { fontSize: 14, color: "#6c7086", fontStyle: "italic" },
|
||||||
|
waitlistCard: {
|
||||||
|
backgroundColor: "rgba(217, 40, 0, 0.15)",
|
||||||
|
borderColor: "#D92800",
|
||||||
|
},
|
||||||
|
approveButton: {
|
||||||
|
backgroundColor: "#a6e3a1",
|
||||||
|
borderColor: "#a6e3a1",
|
||||||
|
minHeight: 36,
|
||||||
|
flex: 1,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
},
|
||||||
|
denyButton: {
|
||||||
|
backgroundColor: "#D92800",
|
||||||
|
borderColor: "#D92800",
|
||||||
|
minHeight: 36,
|
||||||
|
flex: 1,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default PlayersScreen;
|
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,402 +54,503 @@ 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;
|
||||||
}
|
}
|
||||||
setMemberNames(names);
|
setMemberNames(names);
|
||||||
};
|
};
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
if (isBusiness && session.currentSGid) {
|
if (isBusiness && session.currentSGid) {
|
||||||
const subgroup = await getSubgroup(session.currentSGid);
|
const subgroup = await getSubgroup(session.currentSGid);
|
||||||
if (subgroup) teamsAreEnabled = Boolean(subgroup.TeamsEnabled);
|
if (subgroup) teamsAreEnabled = Boolean(subgroup.TeamsEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session.isPendingAdmin && !session.isAcceptedAdmin) {
|
if (session.isPendingAdmin && !session.isAcceptedAdmin) {
|
||||||
const freshUser = await getUser(session.currentUid);
|
const freshUser = await getUser(session.currentUid);
|
||||||
if (freshUser?.IsAcceptedAdmin) {
|
if (freshUser?.IsAcceptedAdmin) {
|
||||||
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);
|
|
||||||
if (waitlist.length > 0) {
|
|
||||||
setOnAdminWaitlist(true);
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setPendingAdmin(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!teamsAreEnabled) {
|
|
||||||
setTeamsDisabled(true);
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setTeamsDisabled(false);
|
|
||||||
|
|
||||||
if (!activeTid) {
|
|
||||||
const waitlist = await getAdminWaitlist(session.currentGid, session.currentUid);
|
|
||||||
if (waitlist.length > 0) {
|
|
||||||
setOnAdminWaitlist(true);
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (activeTid) await loadTeamData(activeTid);
|
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
load();
|
|
||||||
}, [activeTid]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!onAdminWaitlist && !session.isPendingAdmin) return;
|
|
||||||
|
|
||||||
let stopped = false;
|
|
||||||
const refreshWaitlistStatus = async () => {
|
|
||||||
const currentSession = getSession();
|
|
||||||
if (!currentSession.currentUid || !currentSession.currentGid) return;
|
|
||||||
|
|
||||||
const freshUser = await getUser(currentSession.currentUid);
|
|
||||||
if (stopped) return;
|
|
||||||
|
|
||||||
if (freshUser?.IsAcceptedAdmin) {
|
|
||||||
setPendingAdmin(false);
|
|
||||||
setSessionUser(freshUser);
|
|
||||||
setSessionTeam(freshUser.TGid ?? null);
|
|
||||||
navigation.reset({index: 0, routes: [{name: 'Admin'}]});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const waitlist = await getAdminWaitlist(currentSession.currentGid, currentSession.currentUid);
|
|
||||||
if (stopped) return;
|
|
||||||
|
|
||||||
const isStillWaiting = waitlist.length > 0;
|
|
||||||
setOnAdminWaitlist(isStillWaiting);
|
|
||||||
if (!isStillWaiting) {
|
|
||||||
setPendingAdmin(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
refreshWaitlistStatus();
|
|
||||||
const intervalId = setInterval(refreshWaitlistStatus, 5000);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
stopped = true;
|
|
||||||
clearInterval(intervalId);
|
|
||||||
};
|
|
||||||
}, [onAdminWaitlist, session.isPendingAdmin]);
|
|
||||||
|
|
||||||
const handleJoinTeam = async () => {
|
|
||||||
if (!teamCode.trim()) return;
|
|
||||||
// Force uppercase just in case a lowercase code is pasted
|
|
||||||
const code = teamCode.trim().toUpperCase();
|
|
||||||
const result = await joinTeamByCode({
|
|
||||||
JoinCode: code,
|
|
||||||
Uid: session.currentUid,
|
|
||||||
ExpectedSGid: session.currentSGid,
|
|
||||||
});
|
|
||||||
if (!result) return;
|
|
||||||
|
|
||||||
// If the server indicates this code matched the admin join code
|
|
||||||
if (result.adminWaitlist) {
|
|
||||||
setPendingAdmin(true);
|
|
||||||
setOnAdminWaitlist(true);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
const waitlist = await getAdminWaitlist(
|
||||||
setSessionTeam(result.Tid);
|
session.currentGid,
|
||||||
setActiveTid(result.Tid);
|
session.currentUid,
|
||||||
};
|
);
|
||||||
|
if (waitlist.length > 0) {
|
||||||
const handleCreateTeam = async () => {
|
setOnAdminWaitlist(true);
|
||||||
if (!session.currentGid) return;
|
setLoading(false);
|
||||||
const result = await createTeam({Gid: session.currentGid, SGid: session.currentSGid, TeamName: 'My Team'});
|
return;
|
||||||
if (!result) return;
|
|
||||||
const membership = await joinTeamByCode({Tid: result.Tid, Uid: session.currentUid});
|
|
||||||
if (membership) {
|
|
||||||
setSessionTeam(result.Tid);
|
|
||||||
setActiveTid(result.Tid);
|
|
||||||
}
|
}
|
||||||
};
|
setPendingAdmin(false);
|
||||||
|
}
|
||||||
|
|
||||||
const handleLeaveTeam = async () => {
|
if (!teamsAreEnabled) {
|
||||||
const myMembership = members.find((m) => m.Uid === session.currentUid);
|
setTeamsDisabled(true);
|
||||||
if (!myMembership) return;
|
setLoading(false);
|
||||||
await leaveTeam(myMembership.id);
|
return;
|
||||||
await updateUser(session.currentUid, {TGid: null});
|
}
|
||||||
setSessionTeam(null);
|
setTeamsDisabled(false);
|
||||||
setActiveTid(null);
|
|
||||||
setTeam(null);
|
|
||||||
setMembers([]);
|
|
||||||
setMemberNames({});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKickMember = async (member) => {
|
if (!activeTid) {
|
||||||
await leaveTeam(member.id);
|
const waitlist = await getAdminWaitlist(
|
||||||
await loadTeamData(activeTid);
|
session.currentGid,
|
||||||
};
|
session.currentUid,
|
||||||
|
);
|
||||||
const handleRenameTeam = async () => {
|
if (waitlist.length > 0) {
|
||||||
const trimmed = teamNameDraft.trim();
|
setOnAdminWaitlist(true);
|
||||||
if (!trimmed || !activeTid) return;
|
setLoading(false);
|
||||||
const result = await updateTeam(activeTid, {TeamName: trimmed});
|
return;
|
||||||
if (result) {
|
|
||||||
setTeam(result);
|
|
||||||
setEditingName(false);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (activeTid) await loadTeamData(activeTid);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
load();
|
||||||
|
}, [activeTid]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!onAdminWaitlist && !session.isPendingAdmin) return;
|
||||||
|
|
||||||
|
let stopped = false;
|
||||||
|
const refreshWaitlistStatus = async () => {
|
||||||
|
const currentSession = getSession();
|
||||||
|
if (!currentSession.currentUid || !currentSession.currentGid) return;
|
||||||
|
|
||||||
|
const freshUser = await getUser(currentSession.currentUid);
|
||||||
|
if (stopped) return;
|
||||||
|
|
||||||
|
if (freshUser?.IsAcceptedAdmin) {
|
||||||
|
setPendingAdmin(false);
|
||||||
|
setSessionUser(freshUser);
|
||||||
|
setSessionTeam(freshUser.TGid ?? null);
|
||||||
|
navigation.reset({ index: 0, routes: [{ name: "Admin" }] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const waitlist = await getAdminWaitlist(
|
||||||
|
currentSession.currentGid,
|
||||||
|
currentSession.currentUid,
|
||||||
|
);
|
||||||
|
if (stopped) return;
|
||||||
|
|
||||||
|
const isStillWaiting = waitlist.length > 0;
|
||||||
|
setOnAdminWaitlist(isStillWaiting);
|
||||||
|
if (!isStillWaiting) {
|
||||||
|
setPendingAdmin(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if current user is the team leader
|
refreshWaitlistStatus();
|
||||||
|
const intervalId = setInterval(refreshWaitlistStatus, 5000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
stopped = true;
|
||||||
|
clearInterval(intervalId);
|
||||||
|
};
|
||||||
|
}, [onAdminWaitlist, session.isPendingAdmin]);
|
||||||
|
|
||||||
|
const handleJoinTeam = async () => {
|
||||||
|
if (!teamCode.trim()) return;
|
||||||
|
// Force uppercase just in case a lowercase code is pasted
|
||||||
|
const code = teamCode.trim().toUpperCase();
|
||||||
|
const result = await joinTeamByCode({
|
||||||
|
JoinCode: code,
|
||||||
|
Uid: session.currentUid,
|
||||||
|
ExpectedSGid: session.currentSGid,
|
||||||
|
});
|
||||||
|
if (!result) return;
|
||||||
|
|
||||||
|
// If the server indicates this code matched the admin join code
|
||||||
|
if (result.adminWaitlist) {
|
||||||
|
setPendingAdmin(true);
|
||||||
|
setOnAdminWaitlist(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSessionTeam(result.Tid);
|
||||||
|
setActiveTid(result.Tid);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateTeam = async () => {
|
||||||
|
if (!session.currentGid) return;
|
||||||
|
const result = await createTeam({
|
||||||
|
Gid: session.currentGid,
|
||||||
|
SGid: session.currentSGid,
|
||||||
|
TeamName: "My Team",
|
||||||
|
});
|
||||||
|
if (!result) return;
|
||||||
|
const membership = await joinTeamByCode({
|
||||||
|
Tid: result.Tid,
|
||||||
|
Uid: session.currentUid,
|
||||||
|
});
|
||||||
|
if (membership) {
|
||||||
|
setSessionTeam(result.Tid);
|
||||||
|
setActiveTid(result.Tid);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLeaveTeam = async () => {
|
||||||
const myMembership = members.find((m) => m.Uid === session.currentUid);
|
const myMembership = members.find((m) => m.Uid === session.currentUid);
|
||||||
const isCurrentUserLeader = Boolean(myMembership?.IsLeader);
|
if (!myMembership) return;
|
||||||
|
await leaveTeam(myMembership.id);
|
||||||
|
await updateUser(session.currentUid, { TGid: null });
|
||||||
|
setSessionTeam(null);
|
||||||
|
setActiveTid(null);
|
||||||
|
setTeam(null);
|
||||||
|
setMembers([]);
|
||||||
|
setMemberNames({});
|
||||||
|
};
|
||||||
|
|
||||||
// View -----------------------
|
const handleKickMember = async (member) => {
|
||||||
|
await leaveTeam(member.id);
|
||||||
|
await loadTeamData(activeTid);
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
const handleRenameTeam = async () => {
|
||||||
return (
|
const trimmed = teamNameDraft.trim();
|
||||||
<Screen style={styles.center}>
|
if (!trimmed || !activeTid) return;
|
||||||
<ActivityIndicator size="large"/>
|
const result = await updateTeam(activeTid, { TeamName: trimmed });
|
||||||
</Screen>
|
if (result) {
|
||||||
);
|
setTeam(result);
|
||||||
|
setEditingName(false);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (teamsDisabled) {
|
// Check if current user is the team leader
|
||||||
return (
|
const myMembership = members.find((m) => m.Uid === session.currentUid);
|
||||||
<Screen style={styles.center}>
|
const isCurrentUserLeader = Boolean(myMembership?.IsLeader);
|
||||||
<View style={styles.disabledWrap}>
|
|
||||||
<Text style={styles.disabledTitle}>Teams Disabled</Text>
|
|
||||||
<Text style={styles.disabledBody}>The admin has not enabled teams for this game.</Text>
|
|
||||||
</View>
|
|
||||||
</Screen>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Waiting for admin approval
|
// View -----------------------
|
||||||
if (onAdminWaitlist) {
|
|
||||||
return <PendingApprovalView message="Waiting to be approved for Admin Team"/>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not in a team
|
if (loading) {
|
||||||
if (!activeTid || !team) {
|
|
||||||
// Disable team actions until the user has joined a game
|
|
||||||
const notInGame = !session.currentGid;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Screen style={styles.center}>
|
|
||||||
{notInGame && (
|
|
||||||
<Text style={styles.notInGameText}>Join a game first to manage teams.</Text>
|
|
||||||
)}
|
|
||||||
<View style={styles.inputRow}>
|
|
||||||
<TextInput
|
|
||||||
style={[styles.codeInput, notInGame && styles.inputDisabled]}
|
|
||||||
placeholder="Enter Team Code"
|
|
||||||
placeholderTextColor="#9ca3af"
|
|
||||||
value={teamCode}
|
|
||||||
onChangeText={(text) => setTeamCode(text.toUpperCase())}
|
|
||||||
autoCapitalize="characters"
|
|
||||||
editable={!notInGame}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
label="Join"
|
|
||||||
onClick={handleJoinTeam}
|
|
||||||
styleButton={styles.joinButton}
|
|
||||||
styleLabel={styles.joinLabel}
|
|
||||||
disabled={notInGame}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
{!isAdmin && (
|
|
||||||
<View style={styles.fullRow}>
|
|
||||||
<ButtonTray>
|
|
||||||
<Button
|
|
||||||
label="Create Team"
|
|
||||||
onClick={handleCreateTeam}
|
|
||||||
styleButton={styles.createButton}
|
|
||||||
styleLabel={styles.createLabel}
|
|
||||||
disabled={notInGame}
|
|
||||||
/>
|
|
||||||
</ButtonTray>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</Screen>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// In a team
|
|
||||||
return (
|
return (
|
||||||
<Screen style={styles.container}>
|
<Screen style={styles.center}>
|
||||||
<View style={styles.teamHeader}>
|
<ActivityIndicator size="large" />
|
||||||
<Text style={styles.teamCodeLabel}>Team Code:</Text>
|
</Screen>
|
||||||
<Text style={styles.teamCodeValue}>{team.JoinCode}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Team name display / edit */}
|
|
||||||
<View style={styles.teamNameSection}>
|
|
||||||
{editingName ? (
|
|
||||||
<View style={styles.renameRow}>
|
|
||||||
<TextInput
|
|
||||||
style={styles.renameInput}
|
|
||||||
value={teamNameDraft}
|
|
||||||
onChangeText={setTeamNameDraft}
|
|
||||||
placeholder="Team name"
|
|
||||||
placeholderTextColor="#9ca3af"
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
label="Save"
|
|
||||||
onClick={handleRenameTeam}
|
|
||||||
styleButton={styles.renameSaveButton}
|
|
||||||
styleLabel={styles.renameSaveLabel}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
label="Cancel"
|
|
||||||
onClick={() => setEditingName(false)}
|
|
||||||
styleButton={styles.renameCancelButton}
|
|
||||||
styleLabel={styles.renameCancelLabel}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<View style={styles.renameRow}>
|
|
||||||
<Text style={styles.teamNameText}>{team.TeamName || 'Unnamed Team'}</Text>
|
|
||||||
{isCurrentUserLeader && (
|
|
||||||
<Button
|
|
||||||
label="Rename"
|
|
||||||
onClick={() => { setTeamNameDraft(team.TeamName || ''); setEditingName(true); }}
|
|
||||||
styleButton={styles.renameButton}
|
|
||||||
styleLabel={styles.renameLabel}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<ScrollView style={styles.memberList}>
|
|
||||||
{members.map((m) => (
|
|
||||||
// Highlight the current user's card so they can identify themselves
|
|
||||||
<Card key={m.id} style={m.Uid === session.currentUid ? styles.selfCard : undefined}>
|
|
||||||
<View style={styles.memberRow}>
|
|
||||||
<View style={styles.memberInfo}>
|
|
||||||
<Text style={styles.memberName}>
|
|
||||||
{memberNames[m.Uid] || `Player ${m.Uid}`}
|
|
||||||
</Text>
|
|
||||||
{m.IsLeader && <Text style={styles.leaderBadge}>(Team Leader)</Text>}
|
|
||||||
</View>
|
|
||||||
{isCurrentUserLeader && m.Uid !== session.currentUid && (
|
|
||||||
<Button
|
|
||||||
label="Kick"
|
|
||||||
onClick={() => handleKickMember(m)}
|
|
||||||
styleButton={styles.kickButton}
|
|
||||||
styleLabel={styles.kickLabel}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
{members.length === 0 && (
|
|
||||||
<Text style={styles.emptyText}>No members yet.</Text>
|
|
||||||
)}
|
|
||||||
{/* Leave button lives inside the scroll so it is never obscured by the bottom nav bar */}
|
|
||||||
<View style={styles.leaveWrap}>
|
|
||||||
<Button
|
|
||||||
label="Leave Team"
|
|
||||||
onClick={handleLeaveTeam}
|
|
||||||
styleButton={styles.leaveButton}
|
|
||||||
styleLabel={styles.leaveLabel}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</ScrollView>
|
|
||||||
</Screen>
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (teamsDisabled) {
|
||||||
|
return (
|
||||||
|
<Screen style={styles.center}>
|
||||||
|
<View style={styles.disabledWrap}>
|
||||||
|
<Text style={styles.disabledTitle}>Teams Disabled</Text>
|
||||||
|
<Text style={styles.disabledBody}>
|
||||||
|
The admin has not enabled teams for this game.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</Screen>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Waiting for admin approval
|
||||||
|
if (onAdminWaitlist) {
|
||||||
|
return (
|
||||||
|
<PendingApprovalView message="Waiting to be approved for Admin Team" />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not in a team
|
||||||
|
if (!activeTid || !team) {
|
||||||
|
// Disable team actions until the user has joined a game
|
||||||
|
const notInGame = !session.currentGid;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Screen style={styles.center}>
|
||||||
|
{notInGame && (
|
||||||
|
<Text style={styles.notInGameText}>
|
||||||
|
Join a game first to manage teams.
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<View style={styles.inputRow}>
|
||||||
|
<TextInput
|
||||||
|
style={[styles.codeInput, notInGame && styles.inputDisabled]}
|
||||||
|
placeholder="Enter Team Code"
|
||||||
|
placeholderTextColor="#9ca3af"
|
||||||
|
value={teamCode}
|
||||||
|
onChangeText={(text) => setTeamCode(text.toUpperCase())}
|
||||||
|
autoCapitalize="characters"
|
||||||
|
editable={!notInGame}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="Join"
|
||||||
|
onClick={handleJoinTeam}
|
||||||
|
styleButton={styles.joinButton}
|
||||||
|
styleLabel={styles.joinLabel}
|
||||||
|
disabled={notInGame}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
{!isAdmin && (
|
||||||
|
<View style={styles.fullRow}>
|
||||||
|
<ButtonTray>
|
||||||
|
<Button
|
||||||
|
label="Create Team"
|
||||||
|
onClick={handleCreateTeam}
|
||||||
|
styleButton={styles.createButton}
|
||||||
|
styleLabel={styles.createLabel}
|
||||||
|
disabled={notInGame}
|
||||||
|
/>
|
||||||
|
</ButtonTray>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</Screen>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// In a team
|
||||||
|
return (
|
||||||
|
<Screen style={styles.container}>
|
||||||
|
<View style={styles.teamHeader}>
|
||||||
|
<Text style={styles.teamCodeLabel}>Team Code:</Text>
|
||||||
|
<Text style={styles.teamCodeValue}>{team.JoinCode}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Team name display / edit */}
|
||||||
|
<View style={styles.teamNameSection}>
|
||||||
|
{editingName ? (
|
||||||
|
<View style={styles.renameRow}>
|
||||||
|
<TextInput
|
||||||
|
style={styles.renameInput}
|
||||||
|
value={teamNameDraft}
|
||||||
|
onChangeText={setTeamNameDraft}
|
||||||
|
placeholder="Team name"
|
||||||
|
placeholderTextColor="#9ca3af"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="Save"
|
||||||
|
onClick={handleRenameTeam}
|
||||||
|
styleButton={styles.renameSaveButton}
|
||||||
|
styleLabel={styles.renameSaveLabel}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="Cancel"
|
||||||
|
onClick={() => setEditingName(false)}
|
||||||
|
styleButton={styles.renameCancelButton}
|
||||||
|
styleLabel={styles.renameCancelLabel}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View style={styles.renameRow}>
|
||||||
|
<Text style={styles.teamNameText}>
|
||||||
|
{team.TeamName || "Unnamed Team"}
|
||||||
|
</Text>
|
||||||
|
{isCurrentUserLeader && (
|
||||||
|
<Button
|
||||||
|
label="Rename"
|
||||||
|
onClick={() => {
|
||||||
|
setTeamNameDraft(team.TeamName || "");
|
||||||
|
setEditingName(true);
|
||||||
|
}}
|
||||||
|
styleButton={styles.renameButton}
|
||||||
|
styleLabel={styles.renameLabel}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<ScrollView style={styles.memberList}>
|
||||||
|
{members.map((m) => (
|
||||||
|
// Highlight the current user's card so they can identify themselves
|
||||||
|
<Card
|
||||||
|
key={m.id}
|
||||||
|
style={m.Uid === session.currentUid ? styles.selfCard : undefined}
|
||||||
|
>
|
||||||
|
<View style={styles.memberRow}>
|
||||||
|
<View style={styles.memberInfo}>
|
||||||
|
<Text style={styles.memberName}>
|
||||||
|
{memberNames[m.Uid] || `Player ${m.Uid}`}
|
||||||
|
</Text>
|
||||||
|
{m.IsLeader && (
|
||||||
|
<Text style={styles.leaderBadge}>(Team Leader)</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
{isCurrentUserLeader && m.Uid !== session.currentUid && (
|
||||||
|
<Button
|
||||||
|
label="Kick"
|
||||||
|
onClick={() => handleKickMember(m)}
|
||||||
|
styleButton={styles.kickButton}
|
||||||
|
styleLabel={styles.kickLabel}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
{members.length === 0 && (
|
||||||
|
<Text style={styles.emptyText}>No members yet.</Text>
|
||||||
|
)}
|
||||||
|
{/* Leave button lives inside the scroll so it is never obscured by the bottom nav bar */}
|
||||||
|
<View style={styles.leaveWrap}>
|
||||||
|
<Button
|
||||||
|
label="Leave Team"
|
||||||
|
onClick={handleLeaveTeam}
|
||||||
|
styleButton={styles.leaveButton}
|
||||||
|
styleLabel={styles.leaveLabel}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</Screen>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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",
|
||||||
codeInput: {
|
},
|
||||||
flex: 1,
|
disabledBody: { fontSize: 15, color: "#ffffff", textAlign: "center" },
|
||||||
borderWidth: 1,
|
notInGameText: {
|
||||||
borderColor: '#45475a',
|
color: "#ffffff",
|
||||||
borderRadius: 8,
|
fontSize: 15,
|
||||||
paddingHorizontal: 12,
|
fontWeight: "600",
|
||||||
paddingVertical: 10,
|
marginBottom: 16,
|
||||||
fontSize: 16,
|
textAlign: "center",
|
||||||
color: '#ffffff',
|
paddingHorizontal: 20,
|
||||||
backgroundColor: '#313244',
|
},
|
||||||
},
|
inputDisabled: { backgroundColor: "#313244", opacity: 0.5 },
|
||||||
joinButton: {backgroundColor: '#bd93f9', borderColor: '#bd93f9', flex: 0, paddingHorizontal: 20},
|
inputRow: {
|
||||||
joinLabel: {color: '#1e1e2e', fontWeight: '600'},
|
flexDirection: "row",
|
||||||
createButton: {backgroundColor: '#a6e3a1', borderColor: '#a6e3a1'},
|
gap: 10,
|
||||||
createLabel: {color: '#1e1e2e', fontWeight: '600'},
|
marginBottom: 15,
|
||||||
waitlistText: {
|
width: "100%",
|
||||||
fontSize: 18,
|
paddingHorizontal: 20,
|
||||||
fontWeight: '700',
|
},
|
||||||
color: '#ffffff',
|
fullRow: { width: "100%", paddingHorizontal: 20 },
|
||||||
textAlign: 'center',
|
codeInput: {
|
||||||
paddingHorizontal: 30,
|
flex: 1,
|
||||||
},
|
borderWidth: 1,
|
||||||
teamHeader: {
|
borderColor: "#45475a",
|
||||||
backgroundColor: '#45475a',
|
borderRadius: 8,
|
||||||
paddingVertical: 14,
|
paddingHorizontal: 12,
|
||||||
paddingHorizontal: 16,
|
paddingVertical: 10,
|
||||||
flexDirection: 'row',
|
fontSize: 16,
|
||||||
alignItems: 'center',
|
color: "#ffffff",
|
||||||
gap: 8,
|
backgroundColor: "#313244",
|
||||||
},
|
},
|
||||||
teamCodeLabel: {color: '#ffffff', fontSize: 15, fontWeight: '600'},
|
joinButton: {
|
||||||
teamCodeValue: {color: '#ffffff', fontSize: 17, fontWeight: '700', letterSpacing: 2},
|
backgroundColor: "#bd93f9",
|
||||||
teamNameSection: {paddingHorizontal: 15, paddingVertical: 10, borderBottomWidth: 1, borderBottomColor: '#45475a'},
|
borderColor: "#bd93f9",
|
||||||
teamNameText: {fontSize: 18, fontWeight: '700', color: '#ffffff', flex: 1},
|
flex: 0,
|
||||||
renameRow: {flexDirection: 'row', alignItems: 'center', gap: 8},
|
paddingHorizontal: 20,
|
||||||
renameInput: {
|
},
|
||||||
flex: 1,
|
joinLabel: { color: "#1e1e2e", fontWeight: "600" },
|
||||||
borderWidth: 1,
|
createButton: { backgroundColor: "#a6e3a1", borderColor: "#a6e3a1" },
|
||||||
borderColor: '#45475a',
|
createLabel: { color: "#1e1e2e", fontWeight: "600" },
|
||||||
borderRadius: 8,
|
waitlistText: {
|
||||||
paddingHorizontal: 12,
|
fontSize: 18,
|
||||||
paddingVertical: 8,
|
fontWeight: "700",
|
||||||
fontSize: 16,
|
color: "#ffffff",
|
||||||
color: '#ffffff',
|
textAlign: "center",
|
||||||
backgroundColor: '#313244',
|
paddingHorizontal: 30,
|
||||||
},
|
},
|
||||||
renameButton: {backgroundColor: '#bd93f9', borderColor: '#bd93f9', flex: 0, minHeight: 36, paddingHorizontal: 14},
|
teamHeader: {
|
||||||
renameLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 13},
|
backgroundColor: "#45475a",
|
||||||
renameSaveButton: {backgroundColor: '#a6e3a1', borderColor: '#a6e3a1', flex: 0, minHeight: 36, paddingHorizontal: 12},
|
paddingVertical: 14,
|
||||||
renameSaveLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 13},
|
paddingHorizontal: 16,
|
||||||
renameCancelButton: {backgroundColor: '#6c7086', borderColor: '#6c7086', flex: 0, minHeight: 36, paddingHorizontal: 12},
|
flexDirection: "row",
|
||||||
renameCancelLabel: {color: '#cdd6f4', fontWeight: '600', fontSize: 13},
|
alignItems: "center",
|
||||||
memberList: {flex: 1, paddingHorizontal: 15, paddingTop: 10},
|
gap: 8,
|
||||||
memberRow: {
|
},
|
||||||
flexDirection: 'row',
|
teamCodeLabel: { color: "#ffffff", fontSize: 15, fontWeight: "600" },
|
||||||
justifyContent: 'space-between',
|
teamCodeValue: {
|
||||||
alignItems: 'center',
|
color: "#ffffff",
|
||||||
},
|
fontSize: 17,
|
||||||
memberInfo: {flex: 1, flexDirection: 'row', alignItems: 'center', gap: 6},
|
fontWeight: "700",
|
||||||
memberName: {fontSize: 16, color: '#ffffff', fontWeight: '600'},
|
letterSpacing: 2,
|
||||||
leaderBadge: {fontSize: 12, color: '#bd93f9', fontWeight: '700'},
|
},
|
||||||
kickButton: {backgroundColor: '#D92800', borderColor: '#D92800', minHeight: 36, flex: 0, paddingHorizontal: 14},
|
teamNameSection: {
|
||||||
kickLabel: {color: '#1e1e2e', fontWeight: '600', fontSize: 13},
|
paddingHorizontal: 15,
|
||||||
emptyText: {color: '#ffffff', textAlign: 'center', marginTop: 30, fontSize: 14},
|
paddingVertical: 10,
|
||||||
leaveWrap: {paddingHorizontal: 0, paddingTop: 10, paddingBottom: 16},
|
borderBottomWidth: 1,
|
||||||
leaveButton: {backgroundColor: '#D92800', borderColor: '#D92800'},
|
borderBottomColor: "#45475a",
|
||||||
leaveLabel: {color: '#1e1e2e', fontWeight: '600'},
|
},
|
||||||
selfCard: {backgroundColor: 'rgba(166, 227, 161, 0.15)', borderColor: '#a6e3a1'},
|
teamNameText: { fontSize: 18, fontWeight: "700", color: "#ffffff", flex: 1 },
|
||||||
|
renameRow: { flexDirection: "row", alignItems: "center", gap: 8 },
|
||||||
|
renameInput: {
|
||||||
|
flex: 1,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "#45475a",
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 8,
|
||||||
|
fontSize: 16,
|
||||||
|
color: "#ffffff",
|
||||||
|
backgroundColor: "#313244",
|
||||||
|
},
|
||||||
|
renameButton: {
|
||||||
|
backgroundColor: "#bd93f9",
|
||||||
|
borderColor: "#bd93f9",
|
||||||
|
flex: 0,
|
||||||
|
minHeight: 36,
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
},
|
||||||
|
renameLabel: { color: "#1e1e2e", fontWeight: "600", fontSize: 13 },
|
||||||
|
renameSaveButton: {
|
||||||
|
backgroundColor: "#a6e3a1",
|
||||||
|
borderColor: "#a6e3a1",
|
||||||
|
flex: 0,
|
||||||
|
minHeight: 36,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
},
|
||||||
|
renameSaveLabel: { color: "#1e1e2e", fontWeight: "600", fontSize: 13 },
|
||||||
|
renameCancelButton: {
|
||||||
|
backgroundColor: "#6c7086",
|
||||||
|
borderColor: "#6c7086",
|
||||||
|
flex: 0,
|
||||||
|
minHeight: 36,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
},
|
||||||
|
renameCancelLabel: { color: "#cdd6f4", fontWeight: "600", fontSize: 13 },
|
||||||
|
memberList: { flex: 1, paddingHorizontal: 15, paddingTop: 10 },
|
||||||
|
memberRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
memberInfo: { flex: 1, flexDirection: "row", alignItems: "center", gap: 6 },
|
||||||
|
memberName: { fontSize: 16, color: "#ffffff", fontWeight: "600" },
|
||||||
|
leaderBadge: { fontSize: 12, color: "#bd93f9", fontWeight: "700" },
|
||||||
|
kickButton: {
|
||||||
|
backgroundColor: "#D92800",
|
||||||
|
borderColor: "#D92800",
|
||||||
|
minHeight: 36,
|
||||||
|
flex: 0,
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
},
|
||||||
|
kickLabel: { color: "#1e1e2e", fontWeight: "600", fontSize: 13 },
|
||||||
|
emptyText: {
|
||||||
|
color: "#ffffff",
|
||||||
|
textAlign: "center",
|
||||||
|
marginTop: 30,
|
||||||
|
fontSize: 14,
|
||||||
|
},
|
||||||
|
leaveWrap: { paddingHorizontal: 0, paddingTop: 10, paddingBottom: 16 },
|
||||||
|
leaveButton: { backgroundColor: "#D92800", borderColor: "#D92800" },
|
||||||
|
leaveLabel: { color: "#1e1e2e", fontWeight: "600" },
|
||||||
|
selfCard: {
|
||||||
|
backgroundColor: "rgba(166, 227, 161, 0.15)",
|
||||||
|
borderColor: "#a6e3a1",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default TeamScreen;
|
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) {
|
||||||
const cacheName = claimedCache?.CacheName || "Cache";
|
setError(`Failed to claim cache: ${result?.error || "Unknown error"}`);
|
||||||
const points = Number(claimedCache?.CachePoints ?? 0);
|
return;
|
||||||
setClaimedPopupMessage(`${cacheName} claimed! You gained +${points} points.`);
|
|
||||||
setClaimedPopupVisible(true);
|
|
||||||
setTimeout(() => setClaimedPopupVisible(false), 3000);
|
|
||||||
await loadData({ forceRefresh: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Optimistically mark as found — no loadData() to avoid overwriting
|
||||||
|
// this state with a stale server response.
|
||||||
|
setFoundCacheIds((prev) => [...prev, String(cacheId)]);
|
||||||
|
const cacheName = claimedCache?.CacheName || "Cache";
|
||||||
|
setClaimedPopupMessage(
|
||||||
|
`${cacheName} claimed! You gained +${result.points} points.`,
|
||||||
|
);
|
||||||
|
setClaimedPopupVisible(true);
|
||||||
|
setTimeout(() => setClaimedPopupVisible(false), 3000);
|
||||||
},
|
},
|
||||||
[caches, foundCacheIds, loadData, session.currentGlobalPlayerId, setIsClaiming],
|
[caches, foundCacheIds, session.currentGlobalPlayerId, setIsClaiming],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCacheSelect = (cache) => {
|
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,33 +345,32 @@ 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)))
|
||||||
const selected = selectedCacheId === cache.CacheID;
|
.map((cache) => {
|
||||||
return (
|
const selected = selectedCacheId === cache.CacheID;
|
||||||
<Marker
|
return (
|
||||||
key={cache.CacheID}
|
<Marker
|
||||||
coordinate={{
|
key={cache.CacheID}
|
||||||
latitude: cache.CacheLatitude,
|
coordinate={{
|
||||||
longitude: cache.CacheLongitude,
|
latitude: cache.CacheLatitude,
|
||||||
}}
|
longitude: cache.CacheLongitude,
|
||||||
title={cache.CacheName}
|
}}
|
||||||
description={`${cache.CachePoints ?? 0} pts${found ? " · Found" : ""}`}
|
title={cache.CacheName}
|
||||||
pinColor={
|
description={`${cache.CachePoints ?? 0} pts`}
|
||||||
found ? "#22c55e" : selected ? "#f59e0b" : "#ef4444"
|
pinColor={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: false,
|
||||||
isFound: found,
|
})
|
||||||
})
|
}
|
||||||
}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
|
||||||
|
|
||||||
{claimTarget ? (
|
{claimTarget ? (
|
||||||
<Circle
|
<Circle
|
||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+80
-180
@@ -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,56 +129,33 @@ const useGlobalHook = () => {
|
|||||||
// Handlers ----------------------------
|
// Handlers ----------------------------
|
||||||
|
|
||||||
// Events
|
// Events
|
||||||
const getPublicEvents = useCallback(
|
const getPublicEvents = useCallback(async () => {
|
||||||
async (options = {}) => {
|
if (!isGlobalApiReady()) return [];
|
||||||
if (!isGlobalApiReady()) return [];
|
const response = await API.get(withKey(endpoints.events));
|
||||||
return getOrFetchGlobalData(
|
if (!response.isSuccess) return [];
|
||||||
CACHE_KEY.publicEvents,
|
const rows = unwrapList(response.result);
|
||||||
async () => {
|
return rows.filter((e) => e.EventIspublic);
|
||||||
const response = await API.get(withKey(endpoints.events));
|
}, [endpoints.events]);
|
||||||
if (!response.isSuccess) return [];
|
|
||||||
const rows = unwrapList(response.result);
|
|
||||||
return rows.filter((e) => e.EventIspublic);
|
|
||||||
},
|
|
||||||
{ ttlMs: CACHE_TTL.events, forceRefresh: options.forceRefresh },
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[endpoints.events],
|
|
||||||
);
|
|
||||||
|
|
||||||
const 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),
|
if (!response.isSuccess) return null;
|
||||||
async () => {
|
return unwrapSingle(response.result);
|
||||||
const response = await API.get(
|
|
||||||
withKey(`${endpoints.events}/${eventId}`),
|
|
||||||
);
|
|
||||||
if (!response.isSuccess) return null;
|
|
||||||
return unwrapSingle(response.result);
|
|
||||||
},
|
|
||||||
{ ttlMs: CACHE_TTL.events, forceRefresh: options.forceRefresh },
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[endpoints.events],
|
[endpoints.events],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Players
|
// Players
|
||||||
const getPlayersByEvent = useCallback(
|
const getPlayersByEvent = useCallback(
|
||||||
async (eventId, options = {}) => {
|
async (eventId) => {
|
||||||
if (!isGlobalApiReady()) return [];
|
if (!isGlobalApiReady()) return [];
|
||||||
return getOrFetchGlobalData(
|
const response = await API.get(
|
||||||
CACHE_KEY.playersByEvent(eventId),
|
withKey(`${endpoints.players}/events/${eventId}`),
|
||||||
async () => {
|
|
||||||
const response = await API.get(
|
|
||||||
withKey(`${endpoints.players}/events/${eventId}`),
|
|
||||||
);
|
|
||||||
if (!response.isSuccess) return [];
|
|
||||||
return unwrapList(response.result);
|
|
||||||
},
|
|
||||||
{ ttlMs: CACHE_TTL.players, forceRefresh: options.forceRefresh },
|
|
||||||
);
|
);
|
||||||
|
if (!response.isSuccess) return [];
|
||||||
|
return unwrapList(response.result);
|
||||||
},
|
},
|
||||||
[endpoints.players],
|
[endpoints.players],
|
||||||
);
|
);
|
||||||
@@ -214,86 +165,55 @@ 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(
|
const response = await API.get(
|
||||||
CACHE_KEY.cachesByEvent(eventId),
|
withKey(`${endpoints.caches}/events/${eventId}`),
|
||||||
async () => {
|
|
||||||
const response = await API.get(
|
|
||||||
withKey(`${endpoints.caches}/events/${eventId}`),
|
|
||||||
);
|
|
||||||
if (!response.isSuccess) return [];
|
|
||||||
return unwrapList(response.result);
|
|
||||||
},
|
|
||||||
{ ttlMs: CACHE_TTL.caches, forceRefresh: options.forceRefresh },
|
|
||||||
);
|
);
|
||||||
|
if (!response.isSuccess) return [];
|
||||||
|
return unwrapList(response.result);
|
||||||
},
|
},
|
||||||
[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),
|
if (!response.isSuccess) return null;
|
||||||
async () => {
|
return unwrapSingle(response.result);
|
||||||
const response = await API.get(
|
|
||||||
withKey(`${endpoints.caches}/${cacheId}`),
|
|
||||||
);
|
|
||||||
if (!response.isSuccess) return null;
|
|
||||||
return unwrapSingle(response.result);
|
|
||||||
},
|
|
||||||
{ ttlMs: CACHE_TTL.caches, forceRefresh: options.forceRefresh },
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[endpoints.caches],
|
[endpoints.caches],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Finds
|
// Finds
|
||||||
const getFindsByEvent = useCallback(
|
const getFindsByEvent = useCallback(
|
||||||
async (eventId, options = {}) => {
|
async (eventId) => {
|
||||||
if (!isGlobalApiReady()) return [];
|
if (!isGlobalApiReady()) return [];
|
||||||
return getOrFetchGlobalData(
|
const response = await API.get(
|
||||||
CACHE_KEY.findsByEvent(eventId),
|
withKey(`${endpoints.finds}/events/${eventId}`),
|
||||||
async () => {
|
|
||||||
const response = await API.get(
|
|
||||||
withKey(`${endpoints.finds}/events/${eventId}`),
|
|
||||||
);
|
|
||||||
if (!response.isSuccess) return [];
|
|
||||||
return unwrapList(response.result);
|
|
||||||
},
|
|
||||||
{ ttlMs: CACHE_TTL.finds, forceRefresh: options.forceRefresh },
|
|
||||||
);
|
);
|
||||||
|
if (!response.isSuccess) return [];
|
||||||
|
return unwrapList(response.result);
|
||||||
},
|
},
|
||||||
[endpoints.finds],
|
[endpoints.finds],
|
||||||
);
|
);
|
||||||
|
|
||||||
const getFindsByPlayer = useCallback(
|
const getFindsByPlayer = useCallback(
|
||||||
async (playerId, options = {}) => {
|
async (playerId) => {
|
||||||
if (!isGlobalApiReady()) return [];
|
if (!isGlobalApiReady()) return [];
|
||||||
return getOrFetchGlobalData(
|
const response = await API.get(
|
||||||
CACHE_KEY.findsByPlayer(playerId),
|
withKey(`${endpoints.finds}/players/${playerId}`),
|
||||||
async () => {
|
|
||||||
const response = await API.get(
|
|
||||||
withKey(`${endpoints.finds}/players/${playerId}`),
|
|
||||||
);
|
|
||||||
if (!response.isSuccess) return [];
|
|
||||||
return unwrapList(response.result);
|
|
||||||
},
|
|
||||||
{ ttlMs: CACHE_TTL.finds, forceRefresh: options.forceRefresh },
|
|
||||||
);
|
);
|
||||||
|
if (!response.isSuccess) return [];
|
||||||
|
return unwrapList(response.result);
|
||||||
},
|
},
|
||||||
[endpoints.finds],
|
[endpoints.finds],
|
||||||
);
|
);
|
||||||
@@ -303,57 +223,53 @@ 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 [];
|
const response = await API.get(withKey(endpoints.users));
|
||||||
return getOrFetchGlobalData(
|
if (!response.isSuccess) return [];
|
||||||
CACHE_KEY.users,
|
return unwrapList(response.result);
|
||||||
async () => {
|
}, [endpoints.users]);
|
||||||
const response = await API.get(withKey(endpoints.users));
|
|
||||||
if (!response.isSuccess) return [];
|
|
||||||
return unwrapList(response.result);
|
|
||||||
},
|
|
||||||
{ ttlMs: CACHE_TTL.users, forceRefresh: options.forceRefresh },
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[endpoints.users],
|
|
||||||
);
|
|
||||||
|
|
||||||
const getUser = useCallback(
|
const getUser = useCallback(
|
||||||
async (userId, options = {}) => {
|
async (userId) => {
|
||||||
if (!isGlobalApiReady()) return null;
|
if (!isGlobalApiReady()) return null;
|
||||||
|
// Some global API deployments expose list-only user routes.
|
||||||
const key = CACHE_KEY.userById(userId);
|
const byIdResponse = await API.get(
|
||||||
return getOrFetchGlobalData(
|
withKey(`${endpoints.users}/${userId}`),
|
||||||
key,
|
);
|
||||||
async () => {
|
if (byIdResponse.isSuccess) {
|
||||||
// Some global API deployments expose list-only user routes.
|
const single = unwrapSingle(byIdResponse.result);
|
||||||
const byIdResponse = await API.get(
|
if (single) return single;
|
||||||
withKey(`${endpoints.users}/${userId}`),
|
}
|
||||||
);
|
const users = await getUsers();
|
||||||
if (byIdResponse.isSuccess) {
|
return (
|
||||||
const single = unwrapSingle(byIdResponse.result);
|
users.find((user) => String(user.UserID) === String(userId)) || null
|
||||||
if (single) return single;
|
|
||||||
}
|
|
||||||
|
|
||||||
const users = await getUsers({ forceRefresh: options.forceRefresh });
|
|
||||||
return (
|
|
||||||
users.find((user) => String(user.UserID) === String(userId)) || null
|
|
||||||
);
|
|
||||||
},
|
|
||||||
{ ttlMs: CACHE_TTL.users, forceRefresh: options.forceRefresh },
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[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