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