From ca27f8d6ed51a418b0326ae4e14635b34aa9e669 Mon Sep 17 00:00:00 2001 From: mudabbir-ahmad Date: Tue, 17 Mar 2026 21:49:44 +0000 Subject: [PATCH] Add initial JSON data files and implement core page components for game functionality --- DB/DB-instructions.md | 44 ++ DB/admin-waitlist.json | 2 + DB/db.json | 55 +- DB/game-data.json | 16 + DB/game-types.json | 6 + DB/groups.json | 15 + DB/schema.sql | 27 - DB/subgroup-memberships.json | 15 + DB/subgroups.json | 19 + DB/team-members.json | 2 + DB/teams.json | 2 + DB/users.json | 25 + README.md | 58 +- app/API/API.js | 249 +++++++-- app/Controller/LoginLogic.js | 69 +-- app/Model/DbController.js | 502 ++++++++++++++++++ app/Model/GroupModel.js | 32 ++ app/Model/SessionStore.js | 30 ++ app/Model/TeamModel.js | 26 + app/Model/UserModel.js | 30 ++ app/Screens/AdminHubPage.jsx | 70 +++ app/Screens/GameLobbyPage.jsx | 104 +++- app/Screens/GameTypePage.jsx | 56 +- app/Screens/JoinPrivateGamePage.jsx | 71 ++- app/Screens/LoginPage.jsx | 114 ++++ app/Screens/ManagePrivateSubtaskPage.jsx | 100 ++++ app/Screens/ManageSubgroupsPage.jsx | 118 ++++ app/Screens/MapPage.jsx | 44 ++ app/Screens/RegisterPage.jsx | 151 +++++- app/Screens/TeamPage.jsx | 44 ++ app/ViewModel/useAdminViewModel.js | 29 + app/ViewModel/useAuthViewModel.js | 71 +++ app/ViewModel/useGameTypeViewModel.js | 23 + app/ViewModel/useLobbyViewModel.js | 51 ++ .../useManagePrivateSubtaskViewModel.js | 56 ++ app/ViewModel/useMapViewModel.js | 30 ++ app/ViewModel/usePrivateGameViewModel.js | 31 ++ app/ViewModel/useSubgroupAdminViewModel.js | 66 +++ app/ViewModel/useTeamViewModel.js | 33 ++ app/_layout.tsx | 16 +- app/admin-hub.tsx | 4 + app/components/AdminFooterNavbar.jsx | 45 ++ app/components/MemberFooterNavbar.jsx | 49 ++ app/game-lobby.tsx | 4 + app/game-type.tsx | 4 + app/index.tsx | 14 +- app/join-private-game.tsx | 4 + app/login.tsx | 4 + app/manage-private-subtask.tsx | 4 + app/manage-subgroups.tsx | 4 + app/map.tsx | 4 + app/register.tsx | 4 + app/team.tsx | 4 + package-lock.json | 24 - 54 files changed, 2424 insertions(+), 250 deletions(-) create mode 100644 DB/DB-instructions.md create mode 100644 DB/admin-waitlist.json create mode 100644 DB/game-data.json create mode 100644 DB/game-types.json create mode 100644 DB/groups.json delete mode 100644 DB/schema.sql create mode 100644 DB/subgroup-memberships.json create mode 100644 DB/subgroups.json create mode 100644 DB/team-members.json create mode 100644 DB/teams.json create mode 100644 DB/users.json create mode 100644 app/Model/DbController.js create mode 100644 app/Model/GroupModel.js create mode 100644 app/Model/SessionStore.js create mode 100644 app/Model/TeamModel.js create mode 100644 app/Model/UserModel.js create mode 100644 app/Screens/AdminHubPage.jsx create mode 100644 app/Screens/ManagePrivateSubtaskPage.jsx create mode 100644 app/Screens/ManageSubgroupsPage.jsx create mode 100644 app/Screens/MapPage.jsx create mode 100644 app/Screens/TeamPage.jsx create mode 100644 app/ViewModel/useAdminViewModel.js create mode 100644 app/ViewModel/useAuthViewModel.js create mode 100644 app/ViewModel/useGameTypeViewModel.js create mode 100644 app/ViewModel/useLobbyViewModel.js create mode 100644 app/ViewModel/useManagePrivateSubtaskViewModel.js create mode 100644 app/ViewModel/useMapViewModel.js create mode 100644 app/ViewModel/usePrivateGameViewModel.js create mode 100644 app/ViewModel/useSubgroupAdminViewModel.js create mode 100644 app/ViewModel/useTeamViewModel.js create mode 100644 app/admin-hub.tsx create mode 100644 app/components/AdminFooterNavbar.jsx create mode 100644 app/components/MemberFooterNavbar.jsx create mode 100644 app/game-lobby.tsx create mode 100644 app/game-type.tsx create mode 100644 app/join-private-game.tsx create mode 100644 app/login.tsx create mode 100644 app/manage-private-subtask.tsx create mode 100644 app/manage-subgroups.tsx create mode 100644 app/map.tsx create mode 100644 app/register.tsx create mode 100644 app/team.tsx 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 -