Add initial JSON data files and implement core page components for game functionality

This commit is contained in:
bobbert committed 2026-03-17 21:49:44 +00:00
1 parent 81847a9188
commit ca27f8d6ed
54 files changed
+2424 -250

No files matched your search

+29
View File
@@ -0,0 +1,29 @@
import {
approveAdminAccess,
createPrivateGame,
requestAdminAccess,
updateGroupSettings,
} from '../API/API';
import { getSession, setSessionGroup } from '../Model/SessionStore';
const useAdminViewModel = () => {
const createGame = async (payload) => {
const { currentUid } = getSession();
const group = await createPrivateGame({ ...payload, userId: currentUid });
setSessionGroup(group.Gid, 0);
return group;
};
const requestAdmin = async (payload) => requestAdminAccess(payload);
const approveAdmin = async (payload) => approveAdminAccess(payload);
const setTeamsEnabled = async (gid, teamsEnabled) => updateGroupSettings({ gid, teamsEnabled });
return {
createGame,
requestAdmin,
approveAdmin,
setTeamsEnabled,
};
};
export default useAdminViewModel;
+71
View File
@@ -0,0 +1,71 @@
import { useMemo, useState } from 'react';
import { loginUser, registerUser } from '../API/API';
import { setSessionUser } from '../Model/SessionStore';
const useAuthViewModel = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [username, setUsername] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [accountType, setAccountType] = useState('Individual');
const [error, setError] = useState('');
const [currentUser, setCurrentUser] = useState(null);
const canRegister = useMemo(
() => Boolean(username && email && password && confirmPassword),
[username, email, password, confirmPassword],
);
const login = async () => {
setError('');
try {
const user = await loginUser(email.trim(), password);
setCurrentUser(user);
setSessionUser(user);
return user;
} catch (e) {
setError(e.message);
return null;
}
};
const register = async () => {
setError('');
try {
const user = await registerUser({
username: username.trim(),
email: email.trim().toLowerCase(),
password,
confirmPassword,
accountType,
});
setCurrentUser(user);
setSessionUser(user);
return user;
} catch (e) {
setError(e.message);
return null;
}
};
return {
email,
password,
username,
confirmPassword,
accountType,
error,
currentUser,
canRegister,
setEmail,
setPassword,
setUsername,
setConfirmPassword,
setAccountType,
login,
register,
};
};
export default useAuthViewModel;
+23
View File
@@ -0,0 +1,23 @@
import { useEffect, useState } from 'react';
import { getGameTypes } from '../API/API';
const useGameTypeViewModel = () => {
const [gameTypes, setGameTypes] = useState([]);
useEffect(() => {
let mounted = true;
getGameTypes().then((types) => {
if (mounted) {
setGameTypes(types);
}
});
return () => {
mounted = false;
};
}, []);
return { gameTypes };
};
export default useGameTypeViewModel;
+51
View File
@@ -0,0 +1,51 @@
import { useState } from 'react';
import { createTeam, getLobby, joinTeam } from '../API/API';
import { getSession, setSessionTeam } from '../Model/SessionStore';
const useLobbyViewModel = () => {
const [teamCode, setTeamCode] = useState('');
const [error, setError] = useState('');
const loadLobby = async () => {
const { currentGid } = getSession();
return getLobby(currentGid);
};
const createLobbyTeam = async ({ teamName }) => {
setError('');
try {
const { currentUid, currentGid } = getSession();
const team = await createTeam({ userId: currentUid, gid: currentGid, teamName });
setSessionTeam(team.Tid);
return team;
} catch (e) {
setError(e.message);
return null;
}
};
const joinLobbyTeam = async () => {
setError('');
try {
const { currentUid } = getSession();
const team = await joinTeam({ userId: currentUid, teamCode: teamCode.trim().toUpperCase() });
setSessionTeam(team.Tid);
return team;
} catch (e) {
setError(e.message);
return null;
}
};
return {
teamCode,
error,
setTeamCode,
loadLobby,
createLobbyTeam,
joinLobbyTeam,
};
};
export default useLobbyViewModel;
@@ -0,0 +1,56 @@
import { useState } from 'react';
import { createPrivateGame, joinAsAdmin } from '../API/API';
import { getSession, setSessionGroup } from '../Model/SessionStore';
const useManagePrivateSubtaskViewModel = () => {
const [businessOrSchoolName, setBusinessOrSchoolName] = useState('');
const [error, setError] = useState('');
const createNewPrivateGame = async () => {
setError('');
const { currentUid } = getSession();
try {
const group = await createPrivateGame({
userId: currentUid,
groupName: businessOrSchoolName.trim() || 'New Private Game',
businessOrSchoolName: businessOrSchoolName.trim() || 'Unnamed Business',
isBusiness: true,
});
setSessionGroup(group.Gid, 0);
return group;
} catch (e) {
setError(e.message);
return null;
}
};
const joinAdminForPrivate = async () => {
setError('');
const { currentUid } = getSession();
try {
const response = await joinAsAdmin({
userId: currentUid,
businessOrSchoolName: businessOrSchoolName.trim(),
});
const gid = response?.Gid ?? null;
if (gid !== null) {
setSessionGroup(gid, 0);
}
return response;
} catch (e) {
setError(e.message);
return null;
}
};
return {
businessOrSchoolName,
error,
setBusinessOrSchoolName,
createNewPrivateGame,
joinAdminForPrivate,
};
};
export default useManagePrivateSubtaskViewModel;
+30
View File
@@ -0,0 +1,30 @@
import { useEffect, useState } from 'react';
import { getMapPoints } from '../API/API';
import { getSession } from '../Model/SessionStore';
const useMapViewModel = (groupId = null) => {
const [points, setPoints] = useState([]);
const { currentGid, currentSGid } = getSession();
const resolvedGid = groupId ?? currentGid;
useEffect(() => {
if (!resolvedGid) {
setPoints([]);
return;
}
let active = true;
getMapPoints(resolvedGid, currentSGid).then((payload) => {
if (active) {
setPoints(payload);
}
});
return () => {
active = false;
};
}, [resolvedGid, currentSGid]);
return { points };
};
export default useMapViewModel;
+31
View File
@@ -0,0 +1,31 @@
import { useState } from 'react';
import { joinPrivateGame } from '../API/API';
import { getSession, setSessionGroup } from '../Model/SessionStore';
const usePrivateGameViewModel = () => {
const [joinCode, setJoinCode] = useState('');
const [error, setError] = useState('');
const joinWithCode = async () => {
setError('');
try {
const { currentUid } = getSession();
const group = await joinPrivateGame({ userId: currentUid, joinCode: joinCode.trim().toUpperCase() });
setSessionGroup(group.Gid);
return group;
} catch (e) {
setError(e.message);
return null;
}
};
return {
joinCode,
error,
setJoinCode,
joinWithCode,
};
};
export default usePrivateGameViewModel;
@@ -0,0 +1,66 @@
import { useCallback, useEffect, useState } from 'react';
import { createSubgroup, getSubgroups, updateSubgroupCache } from '../API/API';
import { getSession } from '../Model/SessionStore';
const useSubgroupAdminViewModel = () => {
const [subgroups, setSubgroups] = useState([]);
const [subgroupName, setSubgroupName] = useState('');
const [cacheTriggerMeters, setCacheTriggerMeters] = useState('20');
const [error, setError] = useState('');
const { currentGid } = getSession();
const loadSubgroups = useCallback(async () => {
if (!currentGid) {
return;
}
const rows = await getSubgroups(currentGid);
setSubgroups(rows);
}, [currentGid]);
useEffect(() => {
loadSubgroups();
}, [loadSubgroups]);
const addSubgroup = async () => {
setError('');
try {
await createSubgroup({
gid: currentGid,
subgroupName: subgroupName.trim(),
cacheTriggerMeters: Number(cacheTriggerMeters),
});
setSubgroupName('');
await loadSubgroups();
return true;
} catch (e) {
setError(e.message);
return false;
}
};
const updateCacheForSubgroup = async (sgid, triggerMeters) => {
setError('');
try {
await updateSubgroupCache({ gid: currentGid, sgid, triggerMeters });
await loadSubgroups();
return true;
} catch (e) {
setError(e.message);
return false;
}
};
return {
subgroups,
subgroupName,
cacheTriggerMeters,
error,
setSubgroupName,
setCacheTriggerMeters,
addSubgroup,
updateCacheForSubgroup,
};
};
export default useSubgroupAdminViewModel;
+33
View File
@@ -0,0 +1,33 @@
import { useEffect, useState } from 'react';
import { getTeam } from '../API/API';
import { getSession } from '../Model/SessionStore';
const useTeamViewModel = (teamId = null) => {
const [team, setTeam] = useState(null);
const resolvedTeamId = teamId ?? getSession().currentTGid;
useEffect(() => {
if (!resolvedTeamId) {
setTeam(null);
return;
}
let active = true;
getTeam(resolvedTeamId).then((payload) => {
if (active) {
setTeam(payload);
}
}).catch(() => {
if (active) {
setTeam(null);
}
});
return () => {
active = false;
};
}, [resolvedTeamId]);
return { team };
};
export default useTeamViewModel;