diff --git a/DB/DB-instructions.md b/DB/DB-instructions.md
new file mode 100644
index 0000000..37e479f
--- /dev/null
+++ b/DB/DB-instructions.md
@@ -0,0 +1,44 @@
+# Database Structure
+
+This project uses split JSON files in `DB/` so each collection behaves like its own REST endpoint.
+
+## Purpose
+
+- Keep data modular and readable.
+- Mirror server-side endpoint structure before moving to a hosted REST API.
+- Allow the app to load all collections into one in-memory state via `app/Model/DbController.js`.
+
+## Files
+
+- `users.json`: user accounts and role state (`Uid`, `Gid`, `SGid`, `TGid`, `IsAcceptedAdmin`).
+- `groups.json`: top-level game groups (`Gid`), admin approvals, and group settings.
+- `subgroups.json`: subgroup definitions within each group (`SGid` scoped by `Gid`), join codes, cache trigger defaults.
+- `subgroup-memberships.json`: membership bridge from `Uid` to `Gid`/`SGid`.
+- `admin-waitlist.json`: pending admin requests for groups when active admins already exist.
+- `teams.json`: team entities for non-admin subgroup players.
+- `team-members.json`: membership bridge from `Uid` to `Tid`.
+- `game-data.json`: per-group game payloads (geocaches, map/game objects).
+- `game-types.json`: available game type options for UI flow.
+- `db.json`: index file listing split JSON resources.
+
+## ID Rules
+
+- `Uid`: unique user ID.
+- `Gid`: unique group/game ID.
+- `SGid`: subgroup ID within a group (`0` is admin subgroup).
+- `Tid`: unique team ID.
+- New IDs are generated from the max existing value in each endpoint collection (`users`, `groups`, `teams`) so no separate counter file is required.
+
+## Admin Logic
+
+- Group creator is set as admin (`SGid = 0`) and accepted.
+- New admin join requests go into `admin-waitlist.json` if active admins exist.
+- If no active admin exists, the requester is auto-promoted to accepted admin.
+- Group-level `ApprovedAdmins` stores accepted admin `Uid` values.
+
+## Timestamp Format
+
+- `CreatedAt` values are stored at minute precision:
+ - format: `YYYY-MM-DDTHH:mmZ`
+ - example: `2026-03-17T14:42Z`
+
diff --git a/DB/admin-waitlist.json b/DB/admin-waitlist.json
new file mode 100644
index 0000000..7dd4387
--- /dev/null
+++ b/DB/admin-waitlist.json
@@ -0,0 +1,2 @@
+[]
+
diff --git a/DB/db.json b/DB/db.json
index 4f0ae0a..9494e23 100644
--- a/DB/db.json
+++ b/DB/db.json
@@ -1,45 +1,14 @@
{
- "users": [
- {
- "Uid": 1,
- "username": "admin",
- "password": "admin123",
- "Gid": null,
- "SGid": 0,
- "TGid": null
- },
- {
- "Uid": 2,
- "username": "user1",
- "password": "user123",
- "Gid": 1,
- "SGid": 1,
- "TGid": null
- }
- ],
- "groups": [
- {
- "Gid": 1,
- "GroupName": "Public Group"
- }
- ],
- "subgroups": [
- {
- "SGid": 0,
- "SubGroupName": "Admin Subgroup",
- "Gid": null
- },
- {
- "SGid": 1,
- "SubGroupName": "User Subgroup",
- "Gid": 1
- }
- ],
- "user_groups": [
- {
- "Uid": 2,
- "Gid": 1,
- "SGid": 1
- }
- ]
+ "databaseMode": "split-endpoints",
+ "index": {
+ "users": "users.json",
+ "groups": "groups.json",
+ "subgroups": "subgroups.json",
+ "subgroup_memberships": "subgroup-memberships.json",
+ "admin_waitlist": "admin-waitlist.json",
+ "teams": "teams.json",
+ "team_members": "team-members.json",
+ "game_data": "game-data.json",
+ "game_types": "game-types.json"
+ }
}
diff --git a/DB/game-data.json b/DB/game-data.json
new file mode 100644
index 0000000..e333c4d
--- /dev/null
+++ b/DB/game-data.json
@@ -0,0 +1,16 @@
+[
+ {
+ "Gid": 1,
+ "GeoCaches": [
+ {
+ "CacheId": "C1",
+ "Title": "Main Entrance",
+ "Latitude": 0,
+ "Longitude": 0,
+ "TriggerMeters": 20,
+ "SGid": 1
+ }
+ ]
+ }
+]
+
diff --git a/DB/game-types.json b/DB/game-types.json
new file mode 100644
index 0000000..9b3ab44
--- /dev/null
+++ b/DB/game-types.json
@@ -0,0 +1,6 @@
+[
+ "Join Global Game",
+ "Join Private Game",
+ "Manage Private Game"
+]
+
diff --git a/DB/groups.json b/DB/groups.json
new file mode 100644
index 0000000..1909370
--- /dev/null
+++ b/DB/groups.json
@@ -0,0 +1,15 @@
+[
+ {
+ "Gid": 1,
+ "GroupName": "Main Private Game",
+ "BusinessOrSchoolName": "MAD Academy",
+ "GameType": "private",
+ "AdminJoinCode": "A91TQ",
+ "CreatedByUid": 1,
+ "MaxMemberSubgroups": 1,
+ "TeamsEnabled": true,
+ "ApprovedAdmins": [1],
+ "CreatedAt": "2026-03-17T00:00Z"
+ }
+]
+
diff --git a/DB/schema.sql b/DB/schema.sql
deleted file mode 100644
index 0c8ace9..0000000
--- a/DB/schema.sql
+++ /dev/null
@@ -1,27 +0,0 @@
-CREATE TABLE USER (
- Uid INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL,
- username TEXT NOT NULL,
- password TEXT NOT NULL
-);
-
-CREATE TABLE "GROUP" (
- Gid INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL,
- GroupName TEXT NOT NULL
-);
-
-CREATE TABLE SUBGROUP (
- SGid INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL,
- SubGroupName TEXT NOT NULL,
- Gid INTEGER NOT NULL,
- FOREIGN KEY (Gid) REFERENCES "GROUP"(Gid)
-);
-
-CREATE TABLE USER_GROUP (
- Uid INTEGER NOT NULL,
- Gid INTEGER NOT NULL,
- SGid INTEGER,
- PRIMARY KEY (Uid, Gid),
- FOREIGN KEY (Uid) REFERENCES USER(Uid),
- FOREIGN KEY (Gid) REFERENCES "GROUP"(Gid),
- FOREIGN KEY (SGid) REFERENCES SUBGROUP(SGid)
-);
\ No newline at end of file
diff --git a/DB/subgroup-memberships.json b/DB/subgroup-memberships.json
new file mode 100644
index 0000000..7119cff
--- /dev/null
+++ b/DB/subgroup-memberships.json
@@ -0,0 +1,15 @@
+[
+ {
+ "Uid": 1,
+ "Gid": 1,
+ "SGid": 0,
+ "IsAcceptedAdmin": true
+ },
+ {
+ "Uid": 2,
+ "Gid": 1,
+ "SGid": 1,
+ "IsAcceptedAdmin": false
+ }
+]
+
diff --git a/DB/subgroups.json b/DB/subgroups.json
new file mode 100644
index 0000000..baa5636
--- /dev/null
+++ b/DB/subgroups.json
@@ -0,0 +1,19 @@
+[
+ {
+ "SGid": 0,
+ "SubGroupName": "Admins",
+ "Gid": 1,
+ "JoinCode": null,
+ "IsAdminGroup": true,
+ "CacheTriggerMeters": 20
+ },
+ {
+ "SGid": 1,
+ "SubGroupName": "Members",
+ "Gid": 1,
+ "JoinCode": "M4P8R",
+ "IsAdminGroup": false,
+ "CacheTriggerMeters": 20
+ }
+]
+
diff --git a/DB/team-members.json b/DB/team-members.json
new file mode 100644
index 0000000..7dd4387
--- /dev/null
+++ b/DB/team-members.json
@@ -0,0 +1,2 @@
+[]
+
diff --git a/DB/teams.json b/DB/teams.json
new file mode 100644
index 0000000..7dd4387
--- /dev/null
+++ b/DB/teams.json
@@ -0,0 +1,2 @@
+[]
+
diff --git a/DB/users.json b/DB/users.json
new file mode 100644
index 0000000..eb2715d
--- /dev/null
+++ b/DB/users.json
@@ -0,0 +1,25 @@
+[
+ {
+ "Uid": 1,
+ "username": "admin",
+ "email": "admin@mad.local",
+ "passwordHash": "H_145737963",
+ "isBusiness": true,
+ "IsAcceptedAdmin": true,
+ "Gid": 1,
+ "SGid": 0,
+ "TGid": null
+ },
+ {
+ "Uid": 2,
+ "username": "member1",
+ "email": "member1@mad.local",
+ "passwordHash": "H_1322967599",
+ "isBusiness": false,
+ "IsAcceptedAdmin": false,
+ "Gid": 1,
+ "SGid": 1,
+ "TGid": null
+ }
+]
+
diff --git a/README.md b/README.md
index 48dd63f..40e3dbb 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,4 @@
-# Welcome to your Expo app 👋
-
-This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
+# Welcome to the mobile application development repository for our Treasure Hunt Game! This project is built using React Native and Expo.
## Get started
@@ -16,35 +14,49 @@ This is an [Expo](https://expo.dev) project created with [`create-expo-app`](htt
npx expo start
```
-In the output, you'll find options to open the app in a
-- [development build](https://docs.expo.dev/develop/development-builds/introduction/)
-- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
-- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
-- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
-You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
+# Database Structure
-## Get a fresh project
+This project uses split JSON files in `DB/` so each collection behaves like its own REST endpoint.
-When you're ready, run:
+## Purpose
-```bash
-npm run reset-project
-```
+- Keep data modular and readable.
+- Mirror server-side endpoint structure before moving to a hosted REST API.
+- Allow the app to load all collections into one in-memory state via `app/Model/DbController.js`.
-This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.
+## Files
-## Learn more
+- `meta.json`: global version and numeric ID counters.
+- `users.json`: user accounts and role state (`Uid`, `Gid`, `SGid`, `TGid`, `IsAcceptedAdmin`).
+- `groups.json`: top-level game groups (`Gid`), admin approvals, and group settings.
+- `subgroups.json`: subgroup definitions within each group (`SGid` scoped by `Gid`), join codes, cache trigger defaults.
+- `subgroup-memberships.json`: membership bridge from `Uid` to `Gid`/`SGid`.
+- `admin-waitlist.json`: pending admin requests for groups when active admins already exist.
+- `teams.json`: team entities for non-admin subgroup players.
+- `team-members.json`: membership bridge from `Uid` to `Tid`.
+- `game-data.json`: per-group game payloads (geocaches, map/game objects).
+- `game-types.json`: available game type options for UI flow.
+- `db.json`: index file listing split JSON resources.
-To learn more about developing your project with Expo, look at the following resources:
+## ID Rules
-- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
-- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
+- `Uid`: unique user ID.
+- `Gid`: unique group/game ID.
+- `SGid`: subgroup ID within a group (`0` is admin subgroup).
+- `Tid`: unique team ID.
-## Join the community
+## Admin Logic
-Join our community of developers creating universal apps.
+- Group creator is set as admin (`SGid = 0`) and accepted.
+- New admin join requests go into `admin-waitlist.json` if active admins exist.
+- If no active admin exists, the requester is auto-promoted to accepted admin.
+- Group-level `ApprovedAdmins` stores accepted admin `Uid` values.
+
+## Timestamp Format
+
+- `CreatedAt` values are stored at minute precision:
+ - format: `YYYY-MM-DDTHH:mmZ`
+ - example: `2026-03-17T14:42Z`
-- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
-- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
diff --git a/app/API/API.js b/app/API/API.js
index 006e309..59c4854 100644
--- a/app/API/API.js
+++ b/app/API/API.js
@@ -1,48 +1,229 @@
-// This file contains functions to interact with the API or local database.
+import dbController from '../Model/DbController';
-const API_BASE_URL = 'http://localhost:3000'; // Change this to the actual API endpoint when needed
+const apiConfig = {
+ useLocalDb: true,
+ baseUrl: 'http://localhost:3000',
+};
-// Generic fetch function
-const APIFetch = async (endpoint, options = {}) => {
- try {
- const response = await fetch(`${API_BASE_URL}/${endpoint}`, {
- ...options,
- headers: {
- 'Content-Type': 'application/json',
- ...options.headers,
- },
- });
+const setApiConfig = (nextConfig = {}) => {
+ Object.assign(apiConfig, nextConfig);
+};
- if (!response.ok) {
- throw new Error(`API error: ${response.statusText}`);
- }
+const isSameId = (item, id) =>
+ item.id?.toString() === id
+ || item.Uid?.toString() === id
+ || item.Gid?.toString() === id
+ || item.SGid?.toString() === id
+ || item.Tid?.toString() === id;
- return await response.json();
- } catch (error) {
- console.error('Fetch error:', error);
- throw error;
+const normalize = (endpoint) => endpoint.replace(/^\/+/, '').split('/');
+
+const localGet = (endpoint) => {
+ const [resource, id] = normalize(endpoint);
+ const state = dbController.getState();
+
+ if (!resource) {
+ return state;
}
+ if (!state[resource]) {
+ throw new Error(`Resource not found: ${resource}`);
+ }
+ if (!id) {
+ return state[resource];
+ }
+ return state[resource].find((item) => isSameId(item, id)) || null;
};
-// Example function to fetch a user by username
-const fetchUserByUsername = async (username) => {
- return await APIFetch(`users?username=${username}`);
+const localPost = (endpoint, payload) => {
+ const [resource] = normalize(endpoint);
+
+ if (resource === 'auth' && payload.action === 'login') {
+ return dbController.login(payload.data);
+ }
+ if (resource === 'auth' && payload.action === 'register') {
+ return dbController.register(payload.data);
+ }
+ if (resource === 'groups' && payload.action === 'joinPrivate') {
+ return dbController.joinPrivateGame(payload.data);
+ }
+ if (resource === 'groups' && payload.action === 'joinAsAdmin') {
+ return dbController.joinAsAdmin(payload.data);
+ }
+ if (resource === 'groups' && payload.action === 'createPrivate') {
+ return dbController.createPrivateGame(payload.data);
+ }
+ if (resource === 'groups' && payload.action === 'requestAdmin') {
+ return dbController.requestAdminApproval(payload.data);
+ }
+ if (resource === 'groups' && payload.action === 'approveAdmin') {
+ return dbController.approveAdmin(payload.data);
+ }
+ if (resource === 'subgroups' && payload.action === 'create') {
+ return dbController.createSubgroup(payload.data);
+ }
+ if (resource === 'teams' && payload.action === 'create') {
+ return dbController.createTeam(payload.data);
+ }
+ if (resource === 'teams' && payload.action === 'join') {
+ return dbController.joinTeamByCode(payload.data);
+ }
+
+ const state = dbController.getState();
+ if (!state[resource]) {
+ throw new Error(`Resource not found: ${resource}`);
+ }
+ state[resource].push(payload);
+ return payload;
};
-// Example function to fetch all groups
-const fetchGroups = async () => {
- return await APIFetch('groups');
+const localPut = (endpoint, payload) => {
+ const [resource, id] = normalize(endpoint);
+ const state = dbController.getState();
+ if (!state[resource]) {
+ throw new Error(`Resource not found: ${resource}`);
+ }
+ const index = state[resource].findIndex((item) =>
+ isSameId(item, id),
+ );
+ if (index < 0) {
+ throw new Error('Record not found');
+ }
+ state[resource][index] = { ...state[resource][index], ...payload };
+ return state[resource][index];
};
-// Example function to fetch subgroups by group ID
-const fetchSubgroupsByGroupId = async (groupId) => {
- return await APIFetch(`groups/${groupId}/subgroups`);
+const localDelete = (endpoint) => {
+ const [resource, id] = normalize(endpoint);
+ const state = dbController.getState();
+ if (!state[resource]) {
+ throw new Error(`Resource not found: ${resource}`);
+ }
+ const index = state[resource].findIndex((item) =>
+ isSameId(item, id),
+ );
+ if (index < 0) {
+ return { deleted: false };
+ }
+ state[resource].splice(index, 1);
+ return { deleted: true };
};
-// RESTful API functions
-const fetchItems = async (endpoint) => await APIFetch(endpoint);
-const createItem = async (endpoint, data) => await APIFetch(endpoint, { method: 'POST', body: JSON.stringify(data) });
-const updateItem = async (endpoint, data) => await APIFetch(endpoint, { method: 'PUT', body: JSON.stringify(data) });
-const deleteItem = async (endpoint) => await APIFetch(endpoint, { method: 'DELETE' });
+const remoteRequest = async (endpoint, options = {}) => {
+ const response = await fetch(`${apiConfig.baseUrl}/${endpoint.replace(/^\/+/, '')}`, {
+ ...options,
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {}),
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`API error: ${response.status}`);
+ }
+ return response.json();
+};
-export { APIFetch, fetchUserByUsername, fetchGroups, fetchSubgroupsByGroupId, fetchItems, createItem, updateItem, deleteItem };
+const APIFetch = async (endpoint) => {
+ if (apiConfig.useLocalDb) {
+ return localGet(endpoint);
+ }
+ return remoteRequest(endpoint, { method: 'GET' });
+};
+
+const APIPost = async (endpoint, data) => {
+ if (apiConfig.useLocalDb) {
+ return localPost(endpoint, data);
+ }
+ return remoteRequest(endpoint, { method: 'POST', body: JSON.stringify(data) });
+};
+
+const APIPut = async (endpoint, data) => {
+ if (apiConfig.useLocalDb) {
+ if (endpoint === 'groups/settings') {
+ return dbController.updateGroupSettings(data);
+ }
+ if (endpoint === 'subgroups/cache') {
+ return dbController.updateSubgroupCacheSettings(data);
+ }
+ return localPut(endpoint, data);
+ }
+ return remoteRequest(endpoint, { method: 'PUT', body: JSON.stringify(data) });
+};
+
+const APIDelete = async (endpoint) => {
+ if (apiConfig.useLocalDb) {
+ return localDelete(endpoint);
+ }
+ return remoteRequest(endpoint, { method: 'DELETE' });
+};
+
+const loginUser = async (email, password) => APIPost('auth', { action: 'login', data: { email, password } });
+const registerUser = async (payload) => APIPost('auth', { action: 'register', data: payload });
+const getGameTypes = async () => {
+ if (apiConfig.useLocalDb) {
+ return dbController.getGameTypes();
+ }
+ return APIFetch('game_types');
+};
+const joinPrivateGame = async (payload) => APIPost('groups', { action: 'joinPrivate', data: payload });
+const joinAsAdmin = async (payload) => APIPost('groups', { action: 'joinAsAdmin', data: payload });
+const createPrivateGame = async (payload) => APIPost('groups', { action: 'createPrivate', data: payload });
+const requestAdminAccess = async (payload) => APIPost('groups', { action: 'requestAdmin', data: payload });
+const approveAdminAccess = async (payload) => APIPost('groups', { action: 'approveAdmin', data: payload });
+const createSubgroup = async (payload) => APIPost('subgroups', { action: 'create', data: payload });
+const getSubgroups = async (gid) => {
+ if (apiConfig.useLocalDb) {
+ return dbController.getSubgroups(gid);
+ }
+ return APIFetch(`groups/${gid}/subgroups`);
+};
+const updateSubgroupCache = async (payload) => APIPut('subgroups/cache', payload);
+const updateGroupSettings = async (payload) => APIPut('groups/settings', payload);
+const createTeam = async (payload) => APIPost('teams', { action: 'create', data: payload });
+const joinTeam = async (payload) => APIPost('teams', { action: 'join', data: payload });
+const getLobby = async (gid) => {
+ if (apiConfig.useLocalDb) {
+ return dbController.getLobby(gid);
+ }
+ return APIFetch(`groups/${gid}/lobby`);
+};
+const getTeam = async (tid) => {
+ if (apiConfig.useLocalDb) {
+ return dbController.getTeam(tid);
+ }
+ return APIFetch(`teams/${tid}`);
+};
+const getMapPoints = async (gid, sgid = null) => {
+ if (apiConfig.useLocalDb) {
+ return dbController.getMapPoints(gid, sgid);
+ }
+ if (sgid === null || sgid === undefined) {
+ return APIFetch(`groups/${gid}/map`);
+ }
+ return APIFetch(`groups/${gid}/map/${sgid}`);
+};
+
+export {
+ apiConfig,
+ setApiConfig,
+ APIFetch,
+ APIPost,
+ APIPut,
+ APIDelete,
+ loginUser,
+ registerUser,
+ getGameTypes,
+ joinPrivateGame,
+ joinAsAdmin,
+ createPrivateGame,
+ requestAdminAccess,
+ approveAdminAccess,
+ createSubgroup,
+ getSubgroups,
+ updateSubgroupCache,
+ updateGroupSettings,
+ createTeam,
+ joinTeam,
+ getLobby,
+ getTeam,
+ getMapPoints,
+};
diff --git a/app/Controller/LoginLogic.js b/app/Controller/LoginLogic.js
index 8f45d45..7d4adda 100644
--- a/app/Controller/LoginLogic.js
+++ b/app/Controller/LoginLogic.js
@@ -1,59 +1,32 @@
-// If a user's subgroupID is 0, Treat them as ADMIN
-// If a user's subgroupID is 1, Treat them as USER [For Public instances only subgroups avail are 0 and 1]
-// If a user's subgroupID is 2, Treat them as USER [For Private instances, subgroups could range from 0-999 where 0 = admin, non 0 = member group]
-// Use GroupID to link Subgroup to a group in the groups table, where subgroup --> defines permissions and whether the person is allowed to play or not.
-// within subgroups have TeamGroups --> TeamGroups can come and go, they are not a fixed table [Team groups are enabled per admin request.]
-// Team group ==> IF Teams are enabled a user can create a team group, if they're admin in team group they can kick people.
-// All team groups have a unique JOIN CODE.
-// User's table should have their [NULLABLE] unique Gid, SGid and TGid.
-// Gid == GroupID || SGid == SubgroupID || TGid == TeamGroupID
-// When a user logs in, we check their Gid, SGid and TGid to determine their permissions and what they can see on the front end.
-// If a user is in a team group, they can only see other members of that team group and the games that team group is playing.
-// If a user is in a subgroup, they can see all members of that subgroup and the games that subgroup is playing.
-// If a user is in the admin subgroup, they can see all members and all games.
-
import { useState } from 'react';
import { useNavigation } from '@react-navigation/native';
const useLoginLogic = () => {
- const [username, setUsername] = useState('');
+ const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const navigation = useNavigation();
- const dispatch = useDispatch();
const handleLogin = async () => {
- try {
- // Replace with your actual API endpoint
- const response = await fetch('https://yourapi.com/login', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ username, password }),
- });
-
- if (!response.ok) {
- alert("API error");
- }
-
- const data = await response.json();
- // Assuming the API returns user data including Gid, SGid, and TGid
- const { Gid, SGid, TGid } = data;
-
- // Dispatch user data to Redux store
- dispatch(setUser({ Gid, SGid, TGid }));
-
- // Navigate to the appropriate screen based on user role
- if (SGid === 0) {
- navigation.navigate('AdminDashboard');
- } else if (SGid === 1) {
- navigation.navigate('UserDashboard');
- } else {
- navigation.navigate('PrivateDashboard');
- }
- } catch (error) {
- Alert.alert('Login Error', error.message);
+ if (!email || !password) {
+ return false;
}
+
+ if (email.trim().toLowerCase().includes('admin')) {
+ navigation.navigate('admin-hub');
+ return true;
+ }
+
+ navigation.navigate('game-type');
+ return true;
};
-export default LoginLogic;
+ return {
+ email,
+ password,
+ setEmail,
+ setPassword,
+ handleLogin,
+ };
+};
+
+export default useLoginLogic;
diff --git a/app/Model/DbController.js b/app/Model/DbController.js
new file mode 100644
index 0000000..1e14dc9
--- /dev/null
+++ b/app/Model/DbController.js
@@ -0,0 +1,502 @@
+import usersSeed from '../../DB/users.json';
+import groupsSeed from '../../DB/groups.json';
+import subgroupsSeed from '../../DB/subgroups.json';
+import subgroupMembershipsSeed from '../../DB/subgroup-memberships.json';
+import adminWaitlistSeed from '../../DB/admin-waitlist.json';
+import teamsSeed from '../../DB/teams.json';
+import teamMembersSeed from '../../DB/team-members.json';
+import gameDataSeed from '../../DB/game-data.json';
+import gameTypesSeed from '../../DB/game-types.json';
+import UserModel from './UserModel';
+import GroupModel from './GroupModel';
+import TeamModel from './TeamModel';
+
+const buildSeedState = () => ({
+ users: usersSeed,
+ groups: groupsSeed,
+ subgroups: subgroupsSeed,
+ subgroup_memberships: subgroupMembershipsSeed,
+ admin_waitlist: adminWaitlistSeed,
+ teams: teamsSeed,
+ team_members: teamMembersSeed,
+ game_data: gameDataSeed,
+ game_types: gameTypesSeed,
+});
+
+class DbController {
+ constructor(seed = buildSeedState()) {
+ this.seed = JSON.parse(JSON.stringify(seed));
+ this.reset();
+ }
+
+ reset() {
+ this.db = JSON.parse(JSON.stringify(this.seed));
+ }
+
+ hashPassword(value) {
+ let hash = 0;
+ for (let i = 0; i < value.length; i += 1) {
+ hash = (hash << 5) - hash + value.charCodeAt(i);
+ hash |= 0;
+ }
+ return `H_${Math.abs(hash)}`;
+ }
+
+ toMinuteIso(date = new Date()) {
+ return `${date.toISOString().slice(0, 16)}Z`;
+ }
+
+ generateJoinCode(length = 5) {
+ const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
+ let code = '';
+ for (let i = 0; i < length; i += 1) {
+ code += charset[Math.floor(Math.random() * charset.length)];
+ }
+ return code;
+ }
+
+ generateUniqueJoinCode(collection, key) {
+ let code = this.generateJoinCode(Math.random() > 0.5 ? 5 : 6);
+ while (collection.some((item) => item[key] === code)) {
+ code = this.generateJoinCode(Math.random() > 0.5 ? 5 : 6);
+ }
+ return code;
+ }
+
+ nextId(counterKey) {
+ const keyToCollection = {
+ user: { collection: 'users', field: 'Uid' },
+ group: { collection: 'groups', field: 'Gid' },
+ team: { collection: 'teams', field: 'Tid' },
+ };
+
+ const config = keyToCollection[counterKey];
+ if (!config) {
+ throw new Error(`Unsupported counter key: ${counterKey}`);
+ }
+
+ const rows = this.db[config.collection] || [];
+ const maxId = rows.reduce((max, row) => {
+ const value = Number(row[config.field] || 0);
+ return value > max ? value : max;
+ }, 0);
+
+ return maxId + 1;
+ }
+
+ getUserByEmail(email) {
+ return this.db.users.find((u) => u.email.toLowerCase() === email.toLowerCase()) || null;
+ }
+
+ getUserById(uid) {
+ return this.db.users.find((u) => u.Uid === Number(uid)) || null;
+ }
+
+ getGroupById(gid) {
+ return this.db.groups.find((group) => group.Gid === Number(gid)) || null;
+ }
+
+ register({ username, email, password, confirmPassword, accountType }) {
+ if (!username || !email || !password || !confirmPassword) {
+ throw new Error('Missing required registration fields');
+ }
+ if (password !== confirmPassword) {
+ throw new Error('Passwords do not match');
+ }
+ if (this.getUserByEmail(email)) {
+ throw new Error('Email already in use');
+ }
+
+ const uid = this.nextId('user');
+ const user = new UserModel({
+ Uid: uid,
+ username,
+ email,
+ passwordHash: this.hashPassword(password),
+ isBusiness: accountType === 'Business/School',
+ IsAcceptedAdmin: false,
+ Gid: null,
+ SGid: null,
+ TGid: null,
+ });
+
+ this.db.users.push(user.toJSON());
+ return user.toJSON();
+ }
+
+ login({ email, password }) {
+ const user = this.getUserByEmail(email);
+ if (!user) {
+ throw new Error('Email not found');
+ }
+ const incomingHash = this.hashPassword(password);
+ if (incomingHash !== user.passwordHash) {
+ throw new Error('Invalid password');
+ }
+ return user;
+ }
+
+ getGameTypes() {
+ return this.db.game_types;
+ }
+
+ getSubgroupByJoinCode(joinCode) {
+ return this.db.subgroups.find(
+ (subgroup) => subgroup.JoinCode && subgroup.JoinCode === joinCode.toUpperCase(),
+ ) || null;
+ }
+
+ ensureMembership(uid, gid, sgid, isAcceptedAdmin) {
+ const existing = this.db.subgroup_memberships.find(
+ (membership) => membership.Uid === uid && membership.Gid === gid,
+ );
+
+ if (existing) {
+ existing.SGid = sgid;
+ existing.IsAcceptedAdmin = isAcceptedAdmin;
+ return existing;
+ }
+
+ const membership = {
+ Uid: uid,
+ Gid: gid,
+ SGid: sgid,
+ IsAcceptedAdmin: isAcceptedAdmin,
+ };
+ this.db.subgroup_memberships.push(membership);
+ return membership;
+ }
+
+ ensureGroupGameData(gid) {
+ const existing = this.db.game_data.find((entry) => entry.Gid === gid);
+ if (existing) {
+ return existing;
+ }
+ const payload = { Gid: gid, GeoCaches: [] };
+ this.db.game_data.push(payload);
+ return payload;
+ }
+
+ hasActiveAdmin(gid) {
+ return this.db.subgroup_memberships.some(
+ (membership) => membership.Gid === gid && membership.SGid === 0 && membership.IsAcceptedAdmin,
+ );
+ }
+
+ canJoinTeamForSubgroup(group, sgid) {
+ return group.TeamsEnabled && sgid > 0;
+ }
+
+ joinPrivateGame({ userId, joinCode }) {
+ const user = this.getUserById(Number(userId));
+ const subgroup = this.getSubgroupByJoinCode(joinCode);
+
+ if (!user || !subgroup || subgroup.IsAdminGroup) {
+ throw new Error('Invalid subgroup join code');
+ }
+
+ const group = this.getGroupById(subgroup.Gid);
+ if (!group) {
+ throw new Error('Group not found');
+ }
+
+ this.ensureMembership(user.Uid, subgroup.Gid, subgroup.SGid, false);
+ user.Gid = group.Gid;
+ user.SGid = subgroup.SGid;
+ user.IsAcceptedAdmin = false;
+ return group;
+ }
+
+ createPrivateGame({ userId, groupName, businessOrSchoolName, gameType = 'private', isBusiness = false }) {
+ const user = this.getUserById(Number(userId));
+ if (!user) {
+ throw new Error('User not found');
+ }
+
+ const gid = this.nextId('group');
+ const group = new GroupModel({
+ Gid: gid,
+ GroupName: groupName,
+ BusinessOrSchoolName: businessOrSchoolName || groupName,
+ GameType: gameType,
+ AdminJoinCode: this.generateUniqueJoinCode(this.db.groups, 'AdminJoinCode'),
+ CreatedByUid: user.Uid,
+ MaxMemberSubgroups: isBusiness ? 999 : 1,
+ TeamsEnabled: true,
+ ApprovedAdmins: [user.Uid],
+ CreatedAt: this.toMinuteIso(),
+ });
+
+ this.db.groups.push(group.toJSON());
+ this.ensureGroupGameData(gid);
+
+ this.db.subgroups.push({
+ SGid: 0,
+ SubGroupName: 'Admins',
+ Gid: group.Gid,
+ JoinCode: null,
+ IsAdminGroup: true,
+ CacheTriggerMeters: 20,
+ });
+
+ this.createSubgroup({ gid: group.Gid, subgroupName: 'Members', cacheTriggerMeters: 20 });
+
+ this.ensureMembership(user.Uid, group.Gid, 0, true);
+ user.Gid = group.Gid;
+ user.SGid = 0;
+ user.IsAcceptedAdmin = true;
+
+ return group.toJSON();
+ }
+
+ joinAsAdmin({ userId, businessOrSchoolName }) {
+ const user = this.getUserById(Number(userId));
+ const group = this.db.groups.find(
+ (entry) => entry.BusinessOrSchoolName.toLowerCase() === businessOrSchoolName.trim().toLowerCase(),
+ );
+
+ if (!user || !group) {
+ throw new Error('User or business/school not found');
+ }
+
+ this.ensureMembership(user.Uid, group.Gid, 0, false);
+ user.Gid = group.Gid;
+ user.SGid = 0;
+ user.IsAcceptedAdmin = false;
+
+ const activeAdminExists = this.hasActiveAdmin(group.Gid);
+ if (!activeAdminExists) {
+ return this.promoteAdmin({ uid: user.Uid, gid: group.Gid, approvedByUid: user.Uid });
+ }
+
+ const alreadyQueued = this.db.admin_waitlist.some(
+ (entry) => entry.Uid === user.Uid && entry.Gid === group.Gid && entry.Status === 'pending',
+ );
+ if (!alreadyQueued) {
+ this.db.admin_waitlist.push({
+ RequestId: `${group.Gid}_${user.Uid}`,
+ Uid: user.Uid,
+ Gid: group.Gid,
+ RequestedAt: this.toMinuteIso(),
+ Status: 'pending',
+ });
+ }
+
+ return { queued: true, Gid: group.Gid, Uid: user.Uid };
+ }
+
+ promoteAdmin({ uid, gid, approvedByUid }) {
+ const user = this.getUserById(Number(uid));
+ const group = this.getGroupById(Number(gid));
+ if (!user || !group) {
+ throw new Error('User or group not found');
+ }
+
+ this.ensureMembership(user.Uid, group.Gid, 0, true);
+ user.Gid = group.Gid;
+ user.SGid = 0;
+ user.IsAcceptedAdmin = true;
+
+ if (!group.ApprovedAdmins.includes(user.Uid)) {
+ group.ApprovedAdmins.push(user.Uid);
+ }
+ if (!group.ApprovedAdmins.includes(Number(approvedByUid))) {
+ group.ApprovedAdmins.push(Number(approvedByUid));
+ }
+
+ this.db.admin_waitlist = this.db.admin_waitlist.map((entry) => {
+ if (entry.Uid === user.Uid && entry.Gid === group.Gid && entry.Status === 'pending') {
+ return { ...entry, Status: 'approved' };
+ }
+ return entry;
+ });
+
+ return user;
+ }
+
+ requestAdminApproval({ userId, groupId }) {
+ const group = this.getGroupById(Number(groupId));
+ if (!group) {
+ throw new Error('Group not found');
+ }
+ return this.joinAsAdmin({ userId, businessOrSchoolName: group.BusinessOrSchoolName });
+ }
+
+ approveAdmin({ approverUserId, userId, groupId }) {
+ const approverMembership = this.db.subgroup_memberships.find(
+ (membership) => membership.Uid === Number(approverUserId)
+ && membership.Gid === Number(groupId)
+ && membership.SGid === 0
+ && membership.IsAcceptedAdmin,
+ );
+ if (!approverMembership) {
+ throw new Error('Approver is not an admin for this group');
+ }
+ return this.promoteAdmin({ uid: Number(userId), gid: Number(groupId), approvedByUid: Number(approverUserId) });
+ }
+
+ createSubgroup({ gid, subgroupName, cacheTriggerMeters = 20 }) {
+ const group = this.getGroupById(Number(gid));
+ if (!group) {
+ throw new Error('Group not found');
+ }
+
+ const nonAdminCount = this.db.subgroups.filter((entry) => entry.Gid === group.Gid && !entry.IsAdminGroup).length;
+ if (nonAdminCount >= group.MaxMemberSubgroups) {
+ throw new Error('Maximum subgroup count reached');
+ }
+
+ const nextSgid = this.db.subgroups
+ .filter((entry) => entry.Gid === group.Gid && !entry.IsAdminGroup)
+ .reduce((max, entry) => Math.max(max, entry.SGid), 0) + 1;
+
+ const subgroup = {
+ SGid: nextSgid,
+ SubGroupName: subgroupName,
+ Gid: group.Gid,
+ JoinCode: this.generateUniqueJoinCode(this.db.subgroups.filter((entry) => !entry.IsAdminGroup), 'JoinCode'),
+ IsAdminGroup: false,
+ CacheTriggerMeters: cacheTriggerMeters,
+ };
+
+ this.db.subgroups.push(subgroup);
+ return subgroup;
+ }
+
+ getSubgroups(gid) {
+ return this.db.subgroups.filter((subgroup) => subgroup.Gid === Number(gid));
+ }
+
+ updateSubgroupCacheSettings({ gid, sgid, triggerMeters }) {
+ const subgroup = this.db.subgroups.find(
+ (entry) => entry.Gid === Number(gid) && entry.SGid === Number(sgid),
+ );
+ if (!subgroup) {
+ throw new Error('Subgroup not found');
+ }
+ subgroup.CacheTriggerMeters = Number(triggerMeters);
+ return subgroup;
+ }
+
+ createTeam({ userId, gid, teamName }) {
+ const user = this.getUserById(Number(userId));
+ if (!user) {
+ throw new Error('User not found');
+ }
+
+ const group = this.getGroupById(Number(gid));
+ if (!group) {
+ throw new Error('Group not found');
+ }
+ if (!this.canJoinTeamForSubgroup(group, user.SGid || 0)) {
+ throw new Error('Teams are disabled or user is in admin subgroup');
+ }
+
+ const tid = this.nextId('team');
+ const team = new TeamModel({
+ Tid: tid,
+ TeamName: teamName,
+ Gid: group.Gid,
+ JoinCode: this.generateUniqueJoinCode(this.db.teams, 'JoinCode'),
+ CreatorUid: user.Uid,
+ SGid: user.SGid,
+ CreatedAt: this.toMinuteIso(),
+ });
+
+ this.db.teams.push(team.toJSON());
+ this.db.team_members.push({
+ Tid: team.Tid,
+ Uid: user.Uid,
+ IsTeamCreator: true,
+ });
+
+ user.TGid = team.Tid;
+ return team.toJSON();
+ }
+
+ joinTeamByCode({ userId, teamCode }) {
+ const user = this.getUserById(Number(userId));
+ const team = this.db.teams.find((t) => t.JoinCode === teamCode.toUpperCase());
+ if (!user || !team) {
+ throw new Error('Invalid user or team code');
+ }
+
+ const group = this.getGroupById(team.Gid);
+ if (!group || !this.canJoinTeamForSubgroup(group, user.SGid || 0)) {
+ throw new Error('Teams are disabled or user is in admin subgroup');
+ }
+
+ if (user.Gid !== team.Gid) {
+ throw new Error('User is not in this game group');
+ }
+
+ const alreadyInTeam = this.db.team_members.some((tm) => tm.Tid === team.Tid && tm.Uid === user.Uid);
+ if (!alreadyInTeam) {
+ this.db.team_members.push({ Tid: team.Tid, Uid: user.Uid, IsTeamCreator: false });
+ }
+
+ user.TGid = team.Tid;
+ return team;
+ }
+
+ getLobby(gid) {
+ const group = this.getGroupById(Number(gid));
+ if (!group) {
+ return null;
+ }
+
+ const members = this.db.subgroup_memberships
+ .filter((membership) => membership.Gid === group.Gid)
+ .map((membership) => this.getUserById(membership.Uid))
+ .filter(Boolean);
+
+ return {
+ group,
+ members,
+ subgroups: this.getSubgroups(group.Gid),
+ teams: this.db.teams.filter((team) => team.Gid === group.Gid),
+ };
+ }
+
+ getTeam(tid) {
+ const team = this.db.teams.find((entry) => entry.Tid === Number(tid));
+ if (!team) {
+ return null;
+ }
+ const members = this.db.team_members
+ .filter((member) => member.Tid === team.Tid)
+ .map((member) => this.getUserById(member.Uid))
+ .filter(Boolean);
+ return { team, members };
+ }
+
+ getMapPoints(gid, sgid = null) {
+ const gameData = this.db.game_data.find((entry) => entry.Gid === Number(gid));
+ if (!gameData) {
+ return [];
+ }
+ if (sgid === null || sgid === undefined) {
+ return gameData.GeoCaches;
+ }
+ return gameData.GeoCaches.filter((cache) => cache.SGid === Number(sgid));
+ }
+
+ updateGroupSettings({ gid, teamsEnabled }) {
+ const group = this.getGroupById(Number(gid));
+ if (!group) {
+ throw new Error('Group not found');
+ }
+ group.TeamsEnabled = Boolean(teamsEnabled);
+ return group;
+ }
+
+ getState() {
+ return this.db;
+ }
+}
+
+const dbController = new DbController();
+
+export { DbController };
+export default dbController;
+
diff --git a/app/Model/GroupModel.js b/app/Model/GroupModel.js
new file mode 100644
index 0000000..001e493
--- /dev/null
+++ b/app/Model/GroupModel.js
@@ -0,0 +1,32 @@
+class GroupModel {
+ constructor(payload) {
+ this.Gid = payload.Gid;
+ this.GroupName = payload.GroupName;
+ this.BusinessOrSchoolName = payload.BusinessOrSchoolName;
+ this.GameType = payload.GameType;
+ this.AdminJoinCode = payload.AdminJoinCode;
+ this.CreatedByUid = payload.CreatedByUid;
+ this.MaxMemberSubgroups = payload.MaxMemberSubgroups;
+ this.TeamsEnabled = Boolean(payload.TeamsEnabled);
+ this.ApprovedAdmins = payload.ApprovedAdmins || [];
+ this.CreatedAt = payload.CreatedAt;
+ }
+
+ toJSON() {
+ return {
+ Gid: this.Gid,
+ GroupName: this.GroupName,
+ BusinessOrSchoolName: this.BusinessOrSchoolName,
+ GameType: this.GameType,
+ AdminJoinCode: this.AdminJoinCode,
+ CreatedByUid: this.CreatedByUid,
+ MaxMemberSubgroups: this.MaxMemberSubgroups,
+ TeamsEnabled: this.TeamsEnabled,
+ ApprovedAdmins: this.ApprovedAdmins,
+ CreatedAt: this.CreatedAt,
+ };
+ }
+}
+
+export default GroupModel;
+
diff --git a/app/Model/SessionStore.js b/app/Model/SessionStore.js
new file mode 100644
index 0000000..d1e9ea2
--- /dev/null
+++ b/app/Model/SessionStore.js
@@ -0,0 +1,30 @@
+const session = {
+ currentUid: null,
+ currentGid: null,
+ currentSGid: null,
+ currentTGid: null,
+};
+
+const setSessionUser = (user) => {
+ session.currentUid = user?.Uid ?? null;
+ session.currentGid = user?.Gid ?? null;
+ session.currentSGid = user?.SGid ?? null;
+ session.currentTGid = user?.TGid ?? null;
+};
+
+const setSessionGroup = (gid, sgid = null) => {
+ session.currentGid = gid ?? null;
+ if (sgid !== null && sgid !== undefined) {
+ session.currentSGid = sgid;
+ }
+};
+
+const setSessionTeam = (tgid) => {
+ session.currentTGid = tgid ?? null;
+};
+
+const getSession = () => ({ ...session });
+
+export { setSessionUser, setSessionGroup, setSessionTeam, getSession };
+
+
diff --git a/app/Model/TeamModel.js b/app/Model/TeamModel.js
new file mode 100644
index 0000000..a22618e
--- /dev/null
+++ b/app/Model/TeamModel.js
@@ -0,0 +1,26 @@
+class TeamModel {
+ constructor(payload) {
+ this.Tid = payload.Tid;
+ this.TeamName = payload.TeamName;
+ this.Gid = payload.Gid;
+ this.JoinCode = payload.JoinCode;
+ this.CreatorUid = payload.CreatorUid;
+ this.SGid = payload.SGid;
+ this.CreatedAt = payload.CreatedAt;
+ }
+
+ toJSON() {
+ return {
+ Tid: this.Tid,
+ TeamName: this.TeamName,
+ Gid: this.Gid,
+ JoinCode: this.JoinCode,
+ CreatorUid: this.CreatorUid,
+ SGid: this.SGid,
+ CreatedAt: this.CreatedAt,
+ };
+ }
+}
+
+export default TeamModel;
+
diff --git a/app/Model/UserModel.js b/app/Model/UserModel.js
new file mode 100644
index 0000000..42888d2
--- /dev/null
+++ b/app/Model/UserModel.js
@@ -0,0 +1,30 @@
+class UserModel {
+ constructor(payload) {
+ this.Uid = payload.Uid;
+ this.username = payload.username;
+ this.email = payload.email;
+ this.passwordHash = payload.passwordHash;
+ this.isBusiness = Boolean(payload.isBusiness);
+ this.IsAcceptedAdmin = Boolean(payload.IsAcceptedAdmin);
+ this.Gid = payload.Gid ?? null;
+ this.SGid = payload.SGid ?? null;
+ this.TGid = payload.TGid ?? null;
+ }
+
+ toJSON() {
+ return {
+ Uid: this.Uid,
+ username: this.username,
+ email: this.email,
+ passwordHash: this.passwordHash,
+ isBusiness: this.isBusiness,
+ IsAcceptedAdmin: this.IsAcceptedAdmin,
+ Gid: this.Gid,
+ SGid: this.SGid,
+ TGid: this.TGid,
+ };
+ }
+}
+
+export default UserModel;
+
diff --git a/app/Screens/AdminHubPage.jsx b/app/Screens/AdminHubPage.jsx
new file mode 100644
index 0000000..cdd6389
--- /dev/null
+++ b/app/Screens/AdminHubPage.jsx
@@ -0,0 +1,70 @@
+import React from 'react';
+import { View, Text, Pressable, StyleSheet } from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+import AdminFooterNavbar from '../components/AdminFooterNavbar';
+import useAdminViewModel from '../ViewModel/useAdminViewModel';
+
+const AdminHubPage = () => {
+ const navigation = useNavigation();
+ const { createGame } = useAdminViewModel();
+
+ const handleCreateGame = async () => {
+ await createGame({
+ groupName: `Admin Game ${Date.now().toString().slice(-4)}`,
+ businessOrSchoolName: 'New Business',
+ gameType: 'private',
+ isBusiness: true,
+ });
+ navigation.navigate('game-lobby');
+ };
+
+ return (
+
+
+ Admin Hub
+
+ Create a Game
+
+ navigation.navigate('game-lobby')}>
+ Manage Existing Game
+
+ navigation.navigate('manage-subgroups')}>
+ Manage Subgroups
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ wrapper: {
+ flex: 1,
+ backgroundColor: '#ffffff',
+ },
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ padding: 20,
+ gap: 12,
+ },
+ title: {
+ fontSize: 26,
+ fontWeight: '700',
+ color: '#111827',
+ marginBottom: 8,
+ },
+ button: {
+ backgroundColor: '#2563eb',
+ borderRadius: 8,
+ paddingVertical: 12,
+ alignItems: 'center',
+ },
+ buttonText: {
+ color: '#ffffff',
+ fontWeight: '600',
+ },
+});
+
+export default AdminHubPage;
+
diff --git a/app/Screens/GameLobbyPage.jsx b/app/Screens/GameLobbyPage.jsx
index aa4a720..44fc92c 100644
--- a/app/Screens/GameLobbyPage.jsx
+++ b/app/Screens/GameLobbyPage.jsx
@@ -1,15 +1,105 @@
-import React from 'react';
-import { View, Text, Button, TextInput } from 'react-native';
+import React, { useState } from 'react';
+import { View, Text, Pressable, TextInput, StyleSheet } from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+import MemberFooterNavbar from '../components/MemberFooterNavbar';
+import useLobbyViewModel from '../ViewModel/useLobbyViewModel';
const GameLobbyPage = () => {
+ const navigation = useNavigation();
+ const { teamCode, error, setTeamCode, createLobbyTeam, joinLobbyTeam } = useLobbyViewModel();
+ const [hasTeam, setHasTeam] = useState(false);
+
+ const handleCreateTeam = async () => {
+ const team = await createLobbyTeam({
+ teamName: `Team-${Date.now().toString().slice(-4)}`,
+ });
+ if (!team) {
+ return;
+ }
+ setHasTeam(true);
+ navigation.navigate('team');
+ };
+
+ const handleJoinTeam = async () => {
+ const team = await joinLobbyTeam();
+ if (!team) {
+ return;
+ }
+ setHasTeam(true);
+ navigation.navigate('team');
+ };
+
return (
-
- Game Lobby
-