mirror of
https://github.com/mudabbir-ahmad/MAD-Treasure-Hunt.git
synced 2026-10-08 03:00:20 +00:00
Merge pull request
PAGES + DB SETUP + Branch INIT
This commit is contained in:
54 files changed
+2424
-250
No files matched your search
@@ -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`
|
||||||
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[]
|
||||||
|
|
||||||
+12
-43
@@ -1,45 +1,14 @@
|
|||||||
{
|
{
|
||||||
"users": [
|
"databaseMode": "split-endpoints",
|
||||||
{
|
"index": {
|
||||||
"Uid": 1,
|
"users": "users.json",
|
||||||
"username": "admin",
|
"groups": "groups.json",
|
||||||
"password": "admin123",
|
"subgroups": "subgroups.json",
|
||||||
"Gid": null,
|
"subgroup_memberships": "subgroup-memberships.json",
|
||||||
"SGid": 0,
|
"admin_waitlist": "admin-waitlist.json",
|
||||||
"TGid": null
|
"teams": "teams.json",
|
||||||
},
|
"team_members": "team-members.json",
|
||||||
{
|
"game_data": "game-data.json",
|
||||||
"Uid": 2,
|
"game_types": "game-types.json"
|
||||||
"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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"Gid": 1,
|
||||||
|
"GeoCaches": [
|
||||||
|
{
|
||||||
|
"CacheId": "C1",
|
||||||
|
"Title": "Main Entrance",
|
||||||
|
"Latitude": 0,
|
||||||
|
"Longitude": 0,
|
||||||
|
"TriggerMeters": 20,
|
||||||
|
"SGid": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[
|
||||||
|
"Join Global Game",
|
||||||
|
"Join Private Game",
|
||||||
|
"Manage Private Game"
|
||||||
|
]
|
||||||
|
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
@@ -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)
|
|
||||||
);
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"Uid": 1,
|
||||||
|
"Gid": 1,
|
||||||
|
"SGid": 0,
|
||||||
|
"IsAcceptedAdmin": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Uid": 2,
|
||||||
|
"Gid": 1,
|
||||||
|
"SGid": 1,
|
||||||
|
"IsAcceptedAdmin": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[]
|
||||||
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[]
|
||||||
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
# Welcome to your Expo app 👋
|
# Welcome to the mobile application development repository for our Treasure Hunt Game! This project is built using React Native and Expo.
|
||||||
|
|
||||||
This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
|
|
||||||
|
|
||||||
## Get started
|
## Get started
|
||||||
|
|
||||||
@@ -16,35 +14,49 @@ This is an [Expo](https://expo.dev) project created with [`create-expo-app`](htt
|
|||||||
npx expo start
|
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
|
- Keep data modular and readable.
|
||||||
npm run reset-project
|
- 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).
|
- `Uid`: unique user ID.
|
||||||
- [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.
|
- `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.
|
|
||||||
+215
-34
@@ -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 setApiConfig = (nextConfig = {}) => {
|
||||||
const APIFetch = async (endpoint, options = {}) => {
|
Object.assign(apiConfig, nextConfig);
|
||||||
try {
|
};
|
||||||
const response = await fetch(`${API_BASE_URL}/${endpoint}`, {
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...options.headers,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
const isSameId = (item, id) =>
|
||||||
throw new Error(`API error: ${response.statusText}`);
|
item.id?.toString() === id
|
||||||
}
|
|| item.Uid?.toString() === id
|
||||||
|
|| item.Gid?.toString() === id
|
||||||
|
|| item.SGid?.toString() === id
|
||||||
|
|| item.Tid?.toString() === id;
|
||||||
|
|
||||||
return await response.json();
|
const normalize = (endpoint) => endpoint.replace(/^\/+/, '').split('/');
|
||||||
} catch (error) {
|
|
||||||
console.error('Fetch error:', error);
|
const localGet = (endpoint) => {
|
||||||
throw error;
|
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 localPost = (endpoint, payload) => {
|
||||||
const fetchUserByUsername = async (username) => {
|
const [resource] = normalize(endpoint);
|
||||||
return await APIFetch(`users?username=${username}`);
|
|
||||||
|
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 localPut = (endpoint, payload) => {
|
||||||
const fetchGroups = async () => {
|
const [resource, id] = normalize(endpoint);
|
||||||
return await APIFetch('groups');
|
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 localDelete = (endpoint) => {
|
||||||
const fetchSubgroupsByGroupId = async (groupId) => {
|
const [resource, id] = normalize(endpoint);
|
||||||
return await APIFetch(`groups/${groupId}/subgroups`);
|
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 remoteRequest = async (endpoint, options = {}) => {
|
||||||
const fetchItems = async (endpoint) => await APIFetch(endpoint);
|
const response = await fetch(`${apiConfig.baseUrl}/${endpoint.replace(/^\/+/, '')}`, {
|
||||||
const createItem = async (endpoint, data) => await APIFetch(endpoint, { method: 'POST', body: JSON.stringify(data) });
|
...options,
|
||||||
const updateItem = async (endpoint, data) => await APIFetch(endpoint, { method: 'PUT', body: JSON.stringify(data) });
|
headers: {
|
||||||
const deleteItem = async (endpoint) => await APIFetch(endpoint, { method: 'DELETE' });
|
'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,
|
||||||
|
};
|
||||||
@@ -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 { useState } from 'react';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
|
||||||
const useLoginLogic = () => {
|
const useLoginLogic = () => {
|
||||||
const [username, setUsername] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const handleLogin = async () => {
|
const handleLogin = async () => {
|
||||||
try {
|
if (!email || !password) {
|
||||||
// Replace with your actual API endpoint
|
return false;
|
||||||
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.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;
|
||||||
@@ -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;
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
|
||||||
@@ -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 };
|
||||||
|
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
|
||||||
@@ -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 (
|
||||||
|
<View style={styles.wrapper}>
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Text style={styles.title}>Admin Hub</Text>
|
||||||
|
<Pressable style={styles.button} onPress={handleCreateGame}>
|
||||||
|
<Text style={styles.buttonText}>Create a Game</Text>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable style={styles.button} onPress={() => navigation.navigate('game-lobby')}>
|
||||||
|
<Text style={styles.buttonText}>Manage Existing Game</Text>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable style={styles.button} onPress={() => navigation.navigate('manage-subgroups')}>
|
||||||
|
<Text style={styles.buttonText}>Manage Subgroups</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
<AdminFooterNavbar />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
@@ -1,15 +1,105 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { View, Text, Button, TextInput } from 'react-native';
|
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 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 (
|
return (
|
||||||
<View>
|
<View style={styles.wrapper}>
|
||||||
<Text>Game Lobby</Text>
|
<View style={styles.container}>
|
||||||
<Button title="Create Team" onPress={() => {}} />
|
<Text style={styles.title}>Game Lobby</Text>
|
||||||
<Text>Join Team Code:</Text>
|
<TextInput
|
||||||
<TextInput placeholder="Enter Team Join Code" />
|
style={styles.input}
|
||||||
|
placeholder="Join Team Code"
|
||||||
|
autoCapitalize="characters"
|
||||||
|
maxLength={6}
|
||||||
|
value={teamCode}
|
||||||
|
onChangeText={setTeamCode}
|
||||||
|
/>
|
||||||
|
<Pressable style={styles.secondaryButton} onPress={handleJoinTeam}>
|
||||||
|
<Text style={styles.buttonText}>Join Team</Text>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable style={styles.primaryButton} onPress={handleCreateTeam}>
|
||||||
|
<Text style={styles.buttonText}>Create Team</Text>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable style={styles.secondaryButton} onPress={() => navigation.navigate('map')}>
|
||||||
|
<Text style={styles.buttonText}>Open Map</Text>
|
||||||
|
</Pressable>
|
||||||
|
{!!error && <Text style={styles.errorText}>{error}</Text>}
|
||||||
|
</View>
|
||||||
|
<MemberFooterNavbar visible={hasTeam} />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#d1d5db',
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 10,
|
||||||
|
},
|
||||||
|
primaryButton: {
|
||||||
|
backgroundColor: '#2563eb',
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingVertical: 12,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
secondaryButton: {
|
||||||
|
backgroundColor: '#4b5563',
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingVertical: 12,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
buttonText: {
|
||||||
|
color: '#ffffff',
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
color: '#dc2626',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export default GameLobbyPage;
|
export default GameLobbyPage;
|
||||||
@@ -1,14 +1,60 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, Button } from 'react-native';
|
import { View, Text, Pressable, StyleSheet } from 'react-native';
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
import useGameTypeViewModel from '../ViewModel/useGameTypeViewModel';
|
||||||
|
|
||||||
const GameTypePage = () => {
|
const GameTypePage = () => {
|
||||||
|
const navigation = useNavigation();
|
||||||
|
const { gameTypes } = useGameTypeViewModel();
|
||||||
|
|
||||||
|
const navigateByType = (label) => {
|
||||||
|
if (label === 'Join Global Game') {
|
||||||
|
navigation.navigate('game-lobby');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (label === 'Join Private Game') {
|
||||||
|
navigation.navigate('join-private-game');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigation.navigate('manage-private-subtask');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View style={styles.container}>
|
||||||
<Button title="Join Global Game" onPress={() => {}} />
|
<Text style={styles.title}>Select Game Type</Text>
|
||||||
<Button title="Join Private Game" onPress={() => {}} />
|
{gameTypes.map((label) => (
|
||||||
<Button title="Manage Private Game" onPress={() => {}} />
|
<Pressable key={label} style={styles.button} onPress={() => navigateByType(label)}>
|
||||||
|
<Text style={styles.buttonText}>{label}</Text>
|
||||||
|
</Pressable>
|
||||||
|
))}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: 20,
|
||||||
|
gap: 12,
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
},
|
||||||
|
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 GameTypePage;
|
export default GameTypePage;
|
||||||
@@ -1,19 +1,76 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { View, TextInput, Button } from 'react-native';
|
import { View, Text, TextInput, Pressable, StyleSheet } from 'react-native';
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
import usePrivateGameViewModel from '../ViewModel/usePrivateGameViewModel';
|
||||||
|
|
||||||
const JoinPrivateGamePage = () => {
|
const JoinPrivateGamePage = () => {
|
||||||
const [joinCode, setJoinCode] = useState('');
|
const navigation = useNavigation();
|
||||||
|
const { joinCode, error, setJoinCode, joinWithCode } = usePrivateGameViewModel();
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
const handleJoinGame = () => {
|
const handleJoinGame = async () => {
|
||||||
// Logic to check join code and redirect to game lobby
|
setSubmitting(true);
|
||||||
|
const group = await joinWithCode();
|
||||||
|
setSubmitting(false);
|
||||||
|
if (!group) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigation.navigate('game-lobby');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View style={styles.container}>
|
||||||
<TextInput placeholder="Enter Join Code" value={joinCode} onChangeText={setJoinCode} />
|
<Text style={styles.title}>Join Private Game</Text>
|
||||||
<Button title="Join Game" onPress={handleJoinGame} />
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
placeholder="Enter Join Code"
|
||||||
|
autoCapitalize="characters"
|
||||||
|
maxLength={6}
|
||||||
|
value={joinCode}
|
||||||
|
onChangeText={setJoinCode}
|
||||||
|
/>
|
||||||
|
<Pressable style={styles.button} onPress={handleJoinGame}>
|
||||||
|
<Text style={styles.buttonText}>{submitting ? 'Joining...' : 'Join Game'}</Text>
|
||||||
|
</Pressable>
|
||||||
|
{!!error && <Text style={styles.errorText}>{error}</Text>}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: 20,
|
||||||
|
gap: 12,
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 26,
|
||||||
|
fontWeight: '700',
|
||||||
|
color: '#111827',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#d1d5db',
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 10,
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
backgroundColor: '#2563eb',
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingVertical: 12,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
buttonText: {
|
||||||
|
color: '#ffffff',
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
color: '#dc2626',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export default JoinPrivateGamePage;
|
export default JoinPrivateGamePage;
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { View, Text, TextInput, Pressable, StyleSheet } from 'react-native';
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
import useAuthViewModel from '../ViewModel/useAuthViewModel';
|
||||||
|
|
||||||
|
const LoginPage = () => {
|
||||||
|
const navigation = useNavigation();
|
||||||
|
const {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
error,
|
||||||
|
setEmail,
|
||||||
|
setPassword,
|
||||||
|
login,
|
||||||
|
} = useAuthViewModel();
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
setSubmitting(true);
|
||||||
|
const user = await login();
|
||||||
|
setSubmitting(false);
|
||||||
|
if (!user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (user.SGid === 0) {
|
||||||
|
navigation.navigate('admin-hub');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigation.navigate('game-type');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Text style={styles.title}>Login</Text>
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
placeholder="Email"
|
||||||
|
autoCapitalize="none"
|
||||||
|
keyboardType="email-address"
|
||||||
|
value={email}
|
||||||
|
onChangeText={setEmail}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
placeholder="Password"
|
||||||
|
secureTextEntry
|
||||||
|
value={password}
|
||||||
|
onChangeText={setPassword}
|
||||||
|
/>
|
||||||
|
<View style={styles.row}>
|
||||||
|
<Pressable style={styles.leftButton} onPress={() => navigation.navigate('register')}>
|
||||||
|
<Text style={styles.buttonText}>Register</Text>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable style={styles.rightButton} onPress={handleLogin}>
|
||||||
|
<Text style={styles.buttonText}>{submitting ? 'Logging in...' : 'Login'}</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
{!!error && <Text style={styles.errorText}>{error}</Text>}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
padding: 20,
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 12,
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: '700',
|
||||||
|
marginBottom: 8,
|
||||||
|
color: '#111827',
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#d1d5db',
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 10,
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 10,
|
||||||
|
marginTop: 4,
|
||||||
|
},
|
||||||
|
leftButton: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#4b5563',
|
||||||
|
borderRadius: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingVertical: 12,
|
||||||
|
},
|
||||||
|
rightButton: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#2563eb',
|
||||||
|
borderRadius: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingVertical: 12,
|
||||||
|
},
|
||||||
|
buttonText: {
|
||||||
|
color: '#ffffff',
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
color: '#dc2626',
|
||||||
|
marginTop: 4,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default LoginPage;
|
||||||
|
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { View, Text, TextInput, Pressable, StyleSheet } from 'react-native';
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
import useManagePrivateSubtaskViewModel from '../ViewModel/useManagePrivateSubtaskViewModel';
|
||||||
|
|
||||||
|
const ManagePrivateSubtaskPage = () => {
|
||||||
|
const navigation = useNavigation();
|
||||||
|
const {
|
||||||
|
businessOrSchoolName,
|
||||||
|
error,
|
||||||
|
setBusinessOrSchoolName,
|
||||||
|
createNewPrivateGame,
|
||||||
|
joinAdminForPrivate,
|
||||||
|
} = useManagePrivateSubtaskViewModel();
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const handleCreate = async () => {
|
||||||
|
setSubmitting(true);
|
||||||
|
const group = await createNewPrivateGame();
|
||||||
|
setSubmitting(false);
|
||||||
|
if (!group) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigation.navigate('admin-hub');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleJoinAdmin = async () => {
|
||||||
|
setSubmitting(true);
|
||||||
|
const response = await joinAdminForPrivate();
|
||||||
|
setSubmitting(false);
|
||||||
|
if (!response) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigation.navigate('admin-hub');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Text style={styles.title}>Manage Private Game</Text>
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
placeholder="Business/School Name"
|
||||||
|
value={businessOrSchoolName}
|
||||||
|
onChangeText={setBusinessOrSchoolName}
|
||||||
|
/>
|
||||||
|
<Pressable style={styles.primaryButton} onPress={handleCreate}>
|
||||||
|
<Text style={styles.buttonText}>{submitting ? 'Please wait...' : 'Create New Private Game'}</Text>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable style={styles.secondaryButton} onPress={handleJoinAdmin}>
|
||||||
|
<Text style={styles.buttonText}>Join as Admin for Private Game</Text>
|
||||||
|
</Pressable>
|
||||||
|
{!!error && <Text style={styles.errorText}>{error}</Text>}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 12,
|
||||||
|
padding: 20,
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 26,
|
||||||
|
fontWeight: '700',
|
||||||
|
color: '#111827',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#d1d5db',
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 10,
|
||||||
|
},
|
||||||
|
primaryButton: {
|
||||||
|
backgroundColor: '#2563eb',
|
||||||
|
borderRadius: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingVertical: 12,
|
||||||
|
},
|
||||||
|
secondaryButton: {
|
||||||
|
backgroundColor: '#4b5563',
|
||||||
|
borderRadius: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingVertical: 12,
|
||||||
|
},
|
||||||
|
buttonText: {
|
||||||
|
color: '#ffffff',
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
color: '#dc2626',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ManagePrivateSubtaskPage;
|
||||||
|
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { View, Text, TextInput, Pressable, StyleSheet } from 'react-native';
|
||||||
|
import useSubgroupAdminViewModel from '../ViewModel/useSubgroupAdminViewModel';
|
||||||
|
import AdminFooterNavbar from '../components/AdminFooterNavbar';
|
||||||
|
|
||||||
|
const ManageSubgroupsPage = () => {
|
||||||
|
const {
|
||||||
|
subgroups,
|
||||||
|
subgroupName,
|
||||||
|
cacheTriggerMeters,
|
||||||
|
error,
|
||||||
|
setSubgroupName,
|
||||||
|
setCacheTriggerMeters,
|
||||||
|
addSubgroup,
|
||||||
|
updateCacheForSubgroup,
|
||||||
|
} = useSubgroupAdminViewModel();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.wrapper}>
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Text style={styles.title}>Manage Subgroups</Text>
|
||||||
|
{subgroups.map((subgroup) => (
|
||||||
|
<View key={`${subgroup.Gid}_${subgroup.SGid}`} style={styles.row}>
|
||||||
|
<Text style={styles.rowText}>
|
||||||
|
SGid {subgroup.SGid} | {subgroup.SubGroupName} | Code: {subgroup.JoinCode || 'ADMIN'} | Trigger: {subgroup.CacheTriggerMeters}m
|
||||||
|
</Text>
|
||||||
|
<Pressable
|
||||||
|
style={styles.inlineButton}
|
||||||
|
onPress={() => updateCacheForSubgroup(subgroup.SGid, (subgroup.CacheTriggerMeters || 20) + 5)}
|
||||||
|
>
|
||||||
|
<Text style={styles.inlineButtonText}>+5m</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
placeholder="New subgroup name"
|
||||||
|
value={subgroupName}
|
||||||
|
onChangeText={setSubgroupName}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
placeholder="Cache trigger meters"
|
||||||
|
keyboardType="numeric"
|
||||||
|
value={cacheTriggerMeters}
|
||||||
|
onChangeText={setCacheTriggerMeters}
|
||||||
|
/>
|
||||||
|
<Pressable style={styles.button} onPress={addSubgroup}>
|
||||||
|
<Text style={styles.buttonText}>Create Subgroup</Text>
|
||||||
|
</Pressable>
|
||||||
|
{!!error && <Text style={styles.errorText}>{error}</Text>}
|
||||||
|
</View>
|
||||||
|
<AdminFooterNavbar />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
wrapper: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
gap: 10,
|
||||||
|
padding: 20,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: '700',
|
||||||
|
color: '#111827',
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#d1d5db',
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 10,
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
rowText: {
|
||||||
|
color: '#111827',
|
||||||
|
fontSize: 13,
|
||||||
|
},
|
||||||
|
inlineButton: {
|
||||||
|
alignSelf: 'flex-start',
|
||||||
|
backgroundColor: '#2563eb',
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
paddingVertical: 6,
|
||||||
|
borderRadius: 6,
|
||||||
|
},
|
||||||
|
inlineButtonText: {
|
||||||
|
color: '#ffffff',
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#d1d5db',
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 10,
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
backgroundColor: '#2563eb',
|
||||||
|
borderRadius: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingVertical: 12,
|
||||||
|
},
|
||||||
|
buttonText: {
|
||||||
|
color: '#ffffff',
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
color: '#dc2626',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ManageSubgroupsPage;
|
||||||
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { View, Text, StyleSheet } from 'react-native';
|
||||||
|
import MemberFooterNavbar from '../components/MemberFooterNavbar';
|
||||||
|
import useMapViewModel from '../ViewModel/useMapViewModel';
|
||||||
|
|
||||||
|
const MapPage = () => {
|
||||||
|
const { points } = useMapViewModel();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.wrapper}>
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Text style={styles.title}>Map Page</Text>
|
||||||
|
<Text style={styles.body}>Map and hunt markers appear here.</Text>
|
||||||
|
<Text style={styles.body}>Points loaded: {points.length}</Text>
|
||||||
|
</View>
|
||||||
|
<MemberFooterNavbar visible />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
wrapper: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: 20,
|
||||||
|
gap: 6,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 26,
|
||||||
|
fontWeight: '700',
|
||||||
|
color: '#111827',
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
color: '#4b5563',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default MapPage;
|
||||||
|
|
||||||
+132
-19
@@ -1,31 +1,144 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { View, Text, TextInput, Button, Picker } from 'react-native';
|
import { View, Text, TextInput, Pressable, StyleSheet } from 'react-native';
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
import useAuthViewModel from '../ViewModel/useAuthViewModel';
|
||||||
|
|
||||||
const RegisterPage = () => {
|
const RegisterPage = () => {
|
||||||
const [username, setUsername] = useState('');
|
const navigation = useNavigation();
|
||||||
const [email, setEmail] = useState('');
|
const {
|
||||||
const [password, setPassword] = useState('');
|
username,
|
||||||
const [confirmPassword, setConfirmPassword] = useState('');
|
email,
|
||||||
const [userType, setUserType] = useState('Individual');
|
password,
|
||||||
|
confirmPassword,
|
||||||
|
accountType,
|
||||||
|
error,
|
||||||
|
setUsername,
|
||||||
|
setEmail,
|
||||||
|
setPassword,
|
||||||
|
setConfirmPassword,
|
||||||
|
setAccountType,
|
||||||
|
register,
|
||||||
|
} = useAuthViewModel();
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
const handleRegister = () => {
|
const handleRegister = async () => {
|
||||||
// Registration logic here
|
setSubmitting(true);
|
||||||
|
const user = await register();
|
||||||
|
setSubmitting(false);
|
||||||
|
if (!user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigation.navigate(user.SGid === 0 ? 'admin-hub' : 'game-type');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View style={styles.container}>
|
||||||
<Text>Register</Text>
|
<Text style={styles.title}>Register</Text>
|
||||||
<TextInput placeholder="Username" value={username} onChangeText={setUsername} />
|
<TextInput style={styles.input} placeholder="Username" value={username} onChangeText={setUsername} />
|
||||||
<TextInput placeholder="Email" value={email} onChangeText={setEmail} />
|
<TextInput
|
||||||
<TextInput placeholder="Password" value={password} onChangeText={setPassword} secureTextEntry />
|
style={styles.input}
|
||||||
<TextInput placeholder="Confirm Password" value={confirmPassword} onChangeText={setConfirmPassword} secureTextEntry />
|
placeholder="Email"
|
||||||
<Picker selectedValue={userType} onValueChange={(itemValue) => setUserType(itemValue)}>
|
autoCapitalize="none"
|
||||||
<Picker.Item label="Individual" value="Individual" />
|
keyboardType="email-address"
|
||||||
<Picker.Item label="Business/School" value="Business/School" />
|
value={email}
|
||||||
</Picker>
|
onChangeText={setEmail}
|
||||||
<Button title="Register" onPress={handleRegister} />
|
/>
|
||||||
|
<TextInput style={styles.input} placeholder="Password" value={password} onChangeText={setPassword} secureTextEntry />
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
placeholder="Confirm Password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChangeText={setConfirmPassword}
|
||||||
|
secureTextEntry
|
||||||
|
/>
|
||||||
|
<View style={styles.typeRow}>
|
||||||
|
<Pressable
|
||||||
|
style={[styles.typeButton, accountType === 'Individual' && styles.typeButtonActive]}
|
||||||
|
onPress={() => setAccountType('Individual')}
|
||||||
|
>
|
||||||
|
<Text style={[styles.typeText, accountType === 'Individual' && styles.typeTextActive]}>Individual</Text>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable
|
||||||
|
style={[styles.typeButton, accountType === 'Business/School' && styles.typeButtonActive]}
|
||||||
|
onPress={() => setAccountType('Business/School')}
|
||||||
|
>
|
||||||
|
<Text style={[styles.typeText, accountType === 'Business/School' && styles.typeTextActive]}>Business/School</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
<Pressable style={styles.submitButton} onPress={handleRegister}>
|
||||||
|
<Text style={styles.submitText}>{submitting ? 'Creating...' : 'Create Account'}</Text>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable onPress={() => navigation.navigate('login')}>
|
||||||
|
<Text style={styles.backText}>Back to Login</Text>
|
||||||
|
</Pressable>
|
||||||
|
{!!error && <Text style={styles.errorText}>{error}</Text>}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
padding: 20,
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 12,
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: '700',
|
||||||
|
color: '#111827',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#d1d5db',
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 10,
|
||||||
|
},
|
||||||
|
typeRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 10,
|
||||||
|
},
|
||||||
|
typeButton: {
|
||||||
|
flex: 1,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#cbd5e1',
|
||||||
|
borderRadius: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingVertical: 10,
|
||||||
|
},
|
||||||
|
typeButtonActive: {
|
||||||
|
backgroundColor: '#2563eb',
|
||||||
|
borderColor: '#2563eb',
|
||||||
|
},
|
||||||
|
typeText: {
|
||||||
|
color: '#1f2937',
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
typeTextActive: {
|
||||||
|
color: '#ffffff',
|
||||||
|
},
|
||||||
|
submitButton: {
|
||||||
|
backgroundColor: '#2563eb',
|
||||||
|
borderRadius: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingVertical: 12,
|
||||||
|
},
|
||||||
|
submitText: {
|
||||||
|
color: '#ffffff',
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
backText: {
|
||||||
|
textAlign: 'center',
|
||||||
|
color: '#2563eb',
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
textAlign: 'center',
|
||||||
|
color: '#dc2626',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export default RegisterPage;
|
export default RegisterPage;
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { View, Text, StyleSheet } from 'react-native';
|
||||||
|
import MemberFooterNavbar from '../components/MemberFooterNavbar';
|
||||||
|
import useTeamViewModel from '../ViewModel/useTeamViewModel';
|
||||||
|
|
||||||
|
const TeamPage = () => {
|
||||||
|
const { team } = useTeamViewModel();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.wrapper}>
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Text style={styles.title}>Team Page</Text>
|
||||||
|
<Text style={styles.body}>Team details and members appear here.</Text>
|
||||||
|
<Text style={styles.body}>{team?.team?.TeamName || 'No team joined yet'}</Text>
|
||||||
|
</View>
|
||||||
|
<MemberFooterNavbar visible />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
wrapper: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: 20,
|
||||||
|
gap: 6,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 26,
|
||||||
|
fontWeight: '700',
|
||||||
|
color: '#111827',
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
color: '#4b5563',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default TeamPage;
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
|
||||||
+15
-1
@@ -1,5 +1,19 @@
|
|||||||
import { Stack } from "expo-router";
|
import { Stack } from "expo-router";
|
||||||
|
|
||||||
export default function RootLayout() {
|
export default function RootLayout() {
|
||||||
return <Stack />;
|
return (
|
||||||
|
<Stack initialRouteName="index">
|
||||||
|
<Stack.Screen name="index" options={{ headerShown: false }} />
|
||||||
|
<Stack.Screen name="login" options={{ title: "Login" }} />
|
||||||
|
<Stack.Screen name="register" options={{ title: "Register" }} />
|
||||||
|
<Stack.Screen name="game-type" options={{ title: "Select Game Type" }} />
|
||||||
|
<Stack.Screen name="join-private-game" options={{ title: "Join Private Game" }} />
|
||||||
|
<Stack.Screen name="manage-private-subtask" options={{ title: "Manage Private Game" }} />
|
||||||
|
<Stack.Screen name="game-lobby" options={{ title: "Game Lobby" }} />
|
||||||
|
<Stack.Screen name="team" options={{ title: "Team" }} />
|
||||||
|
<Stack.Screen name="map" options={{ title: "Map" }} />
|
||||||
|
<Stack.Screen name="admin-hub" options={{ title: "Admin Hub" }} />
|
||||||
|
<Stack.Screen name="manage-subgroups" options={{ title: "Manage Subgroups" }} />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import AdminHubPage from './Screens/AdminHubPage';
|
||||||
|
|
||||||
|
export default AdminHubPage;
|
||||||
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { View, Pressable, Text, StyleSheet } from 'react-native';
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
|
||||||
|
const AdminFooterNavbar = () => {
|
||||||
|
const navigation = useNavigation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Pressable style={styles.button} onPress={() => navigation.navigate('admin-hub')}>
|
||||||
|
<Text style={styles.text}>Admin Hub</Text>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable style={styles.button} onPress={() => navigation.navigate('game-lobby')}>
|
||||||
|
<Text style={styles.text}>Manage Game</Text>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable style={styles.button} onPress={() => navigation.navigate('team')}>
|
||||||
|
<Text style={styles.text}>Manage Team</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderColor: '#d4d4d8',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 10,
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
flex: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#111827',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default AdminFooterNavbar;
|
||||||
|
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { View, Pressable, Text, StyleSheet } from 'react-native';
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
|
||||||
|
const MemberFooterNavbar = ({ visible = false }) => {
|
||||||
|
const navigation = useNavigation();
|
||||||
|
|
||||||
|
if (!visible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Pressable style={styles.button} onPress={() => navigation.navigate('game-lobby')}>
|
||||||
|
<Text style={styles.text}>Lobby</Text>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable style={styles.button} onPress={() => navigation.navigate('team')}>
|
||||||
|
<Text style={styles.text}>Team</Text>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable style={styles.button} onPress={() => navigation.navigate('map')}>
|
||||||
|
<Text style={styles.text}>Map</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderColor: '#d4d4d8',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 10,
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
flex: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#111827',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default MemberFooterNavbar;
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import GameLobbyPage from './Screens/GameLobbyPage';
|
||||||
|
|
||||||
|
export default GameLobbyPage;
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import GameTypePage from './Screens/GameTypePage';
|
||||||
|
|
||||||
|
export default GameTypePage;
|
||||||
|
|
||||||
+2
-12
@@ -1,15 +1,5 @@
|
|||||||
import { Text, View } from "react-native";
|
import { Redirect } from "expo-router";
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
return (
|
return <Redirect href="/login" />;
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text>Cat testing</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import JoinPrivateGamePage from './Screens/JoinPrivateGamePage';
|
||||||
|
|
||||||
|
export default JoinPrivateGamePage;
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import LoginPage from './Screens/LoginPage';
|
||||||
|
|
||||||
|
export default LoginPage;
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import ManagePrivateSubtaskPage from './Screens/ManagePrivateSubtaskPage';
|
||||||
|
|
||||||
|
export default ManagePrivateSubtaskPage;
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import ManageSubgroupsPage from './Screens/ManageSubgroupsPage';
|
||||||
|
|
||||||
|
export default ManageSubgroupsPage;
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import MapPage from './Screens/MapPage';
|
||||||
|
|
||||||
|
export default MapPage;
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import RegisterPage from './Screens/RegisterPage';
|
||||||
|
|
||||||
|
export default RegisterPage;
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import TeamPage from './Screens/TeamPage';
|
||||||
|
|
||||||
|
export default TeamPage;
|
||||||
|
|
||||||
Generated
-24
@@ -79,7 +79,6 @@
|
|||||||
"version": "7.29.0",
|
"version": "7.29.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
|
||||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.0",
|
"@babel/code-frame": "^7.29.0",
|
||||||
"@babel/generator": "^7.29.0",
|
"@babel/generator": "^7.29.0",
|
||||||
@@ -1367,7 +1366,6 @@
|
|||||||
"version": "7.28.6",
|
"version": "7.28.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
|
||||||
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
|
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
@@ -2830,7 +2828,6 @@
|
|||||||
"version": "7.1.33",
|
"version": "7.1.33",
|
||||||
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.33.tgz",
|
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.33.tgz",
|
||||||
"integrity": "sha512-DpFdWGcgLajKZ1TuIvDNQsblN2QaUFWpTQaB8v7WRP9Mix8H/6TFoIrZd93pbymI2hybd6UYrD+lI408eWVcfw==",
|
"integrity": "sha512-DpFdWGcgLajKZ1TuIvDNQsblN2QaUFWpTQaB8v7WRP9Mix8H/6TFoIrZd93pbymI2hybd6UYrD+lI408eWVcfw==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-navigation/core": "^7.16.1",
|
"@react-navigation/core": "^7.16.1",
|
||||||
"escape-string-regexp": "^4.0.0",
|
"escape-string-regexp": "^4.0.0",
|
||||||
@@ -3008,7 +3005,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.17.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.17.tgz",
|
||||||
"integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==",
|
"integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
@@ -3073,7 +3069,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz",
|
||||||
"integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==",
|
"integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.57.0",
|
"@typescript-eslint/scope-manager": "8.57.0",
|
||||||
"@typescript-eslint/types": "8.57.0",
|
"@typescript-eslint/types": "8.57.0",
|
||||||
@@ -3622,7 +3617,6 @@
|
|||||||
"version": "8.16.0",
|
"version": "8.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -4262,7 +4256,6 @@
|
|||||||
"url": "https://github.com/sponsors/ai"
|
"url": "https://github.com/sponsors/ai"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@@ -5160,7 +5153,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz",
|
||||||
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
|
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -5348,7 +5340,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
|
||||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rtsao/scc": "^1.1.0",
|
"@rtsao/scc": "^1.1.0",
|
||||||
"array-includes": "^3.1.9",
|
"array-includes": "^3.1.9",
|
||||||
@@ -5572,7 +5563,6 @@
|
|||||||
"version": "54.0.33",
|
"version": "54.0.33",
|
||||||
"resolved": "https://registry.npmjs.org/expo/-/expo-54.0.33.tgz",
|
"resolved": "https://registry.npmjs.org/expo/-/expo-54.0.33.tgz",
|
||||||
"integrity": "sha512-3yOEfAKqo+gqHcV8vKcnq0uA5zxlohnhA3fu4G43likN8ct5ZZ3LjAh9wDdKteEkoad3tFPvwxmXW711S5OHUw==",
|
"integrity": "sha512-3yOEfAKqo+gqHcV8vKcnq0uA5zxlohnhA3fu4G43likN8ct5ZZ3LjAh9wDdKteEkoad3tFPvwxmXW711S5OHUw==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.0",
|
"@babel/runtime": "^7.20.0",
|
||||||
"@expo/cli": "54.0.23",
|
"@expo/cli": "54.0.23",
|
||||||
@@ -5638,7 +5628,6 @@
|
|||||||
"version": "18.0.13",
|
"version": "18.0.13",
|
||||||
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.13.tgz",
|
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.13.tgz",
|
||||||
"integrity": "sha512-FnZn12E1dRYKDHlAdIyNFhBurKTS3F9CrfrBDJI5m3D7U17KBHMQ6JEfYlSj7LG7t+Ulr+IKaj58L1k5gBwTcQ==",
|
"integrity": "sha512-FnZn12E1dRYKDHlAdIyNFhBurKTS3F9CrfrBDJI5m3D7U17KBHMQ6JEfYlSj7LG7t+Ulr+IKaj58L1k5gBwTcQ==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/config": "~12.0.13",
|
"@expo/config": "~12.0.13",
|
||||||
"@expo/env": "~2.0.8"
|
"@expo/env": "~2.0.8"
|
||||||
@@ -5661,7 +5650,6 @@
|
|||||||
"version": "14.0.11",
|
"version": "14.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.11.tgz",
|
||||||
"integrity": "sha512-ga0q61ny4s/kr4k8JX9hVH69exVSIfcIc19+qZ7gt71Mqtm7xy2c6kwsPTCyhBW2Ro5yXTT8EaZOpuRi35rHbg==",
|
"integrity": "sha512-ga0q61ny4s/kr4k8JX9hVH69exVSIfcIc19+qZ7gt71Mqtm7xy2c6kwsPTCyhBW2Ro5yXTT8EaZOpuRi35rHbg==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fontfaceobserver": "^2.1.0"
|
"fontfaceobserver": "^2.1.0"
|
||||||
},
|
},
|
||||||
@@ -5708,7 +5696,6 @@
|
|||||||
"version": "8.0.11",
|
"version": "8.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.11.tgz",
|
||||||
"integrity": "sha512-+VSaNL5om3kOp/SSKO5qe6cFgfSIWnnQDSbA7XLs3ECkYzXRquk5unxNS3pg7eK5kNUmQ4kgLI7MhTggAEUBLA==",
|
"integrity": "sha512-+VSaNL5om3kOp/SSKO5qe6cFgfSIWnnQDSbA7XLs3ECkYzXRquk5unxNS3pg7eK5kNUmQ4kgLI7MhTggAEUBLA==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"expo-constants": "~18.0.12",
|
"expo-constants": "~18.0.12",
|
||||||
"invariant": "^2.2.4"
|
"invariant": "^2.2.4"
|
||||||
@@ -9406,7 +9393,6 @@
|
|||||||
"version": "19.1.0",
|
"version": "19.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -9424,7 +9410,6 @@
|
|||||||
"version": "19.1.0",
|
"version": "19.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.26.0"
|
"scheduler": "^0.26.0"
|
||||||
},
|
},
|
||||||
@@ -9457,7 +9442,6 @@
|
|||||||
"version": "0.81.5",
|
"version": "0.81.5",
|
||||||
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.5.tgz",
|
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.5.tgz",
|
||||||
"integrity": "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw==",
|
"integrity": "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jest/create-cache-key-function": "^29.7.0",
|
"@jest/create-cache-key-function": "^29.7.0",
|
||||||
"@react-native/assets-registry": "0.81.5",
|
"@react-native/assets-registry": "0.81.5",
|
||||||
@@ -9514,7 +9498,6 @@
|
|||||||
"version": "2.28.0",
|
"version": "2.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.28.0.tgz",
|
||||||
"integrity": "sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A==",
|
"integrity": "sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@egjs/hammerjs": "^2.0.17",
|
"@egjs/hammerjs": "^2.0.17",
|
||||||
"hoist-non-react-statics": "^3.3.0",
|
"hoist-non-react-statics": "^3.3.0",
|
||||||
@@ -9538,7 +9521,6 @@
|
|||||||
"version": "4.1.6",
|
"version": "4.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.6.tgz",
|
||||||
"integrity": "sha512-F+ZJBYiok/6Jzp1re75F/9aLzkgoQCOh4yxrnwATa8392RvM3kx+fiXXFvwcgE59v48lMwd9q0nzF1oJLXpfxQ==",
|
"integrity": "sha512-F+ZJBYiok/6Jzp1re75F/9aLzkgoQCOh4yxrnwATa8392RvM3kx+fiXXFvwcgE59v48lMwd9q0nzF1oJLXpfxQ==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react-native-is-edge-to-edge": "^1.2.1",
|
"react-native-is-edge-to-edge": "^1.2.1",
|
||||||
"semver": "7.7.2"
|
"semver": "7.7.2"
|
||||||
@@ -9565,7 +9547,6 @@
|
|||||||
"version": "5.6.2",
|
"version": "5.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz",
|
||||||
"integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==",
|
"integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==",
|
||||||
"peer": true,
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "*",
|
"react": "*",
|
||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
@@ -9575,7 +9556,6 @@
|
|||||||
"version": "4.16.0",
|
"version": "4.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.16.0.tgz",
|
||||||
"integrity": "sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==",
|
"integrity": "sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react-freeze": "^1.0.0",
|
"react-freeze": "^1.0.0",
|
||||||
"react-native-is-edge-to-edge": "^1.2.1",
|
"react-native-is-edge-to-edge": "^1.2.1",
|
||||||
@@ -9590,7 +9570,6 @@
|
|||||||
"version": "0.21.2",
|
"version": "0.21.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz",
|
||||||
"integrity": "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==",
|
"integrity": "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.18.6",
|
"@babel/runtime": "^7.18.6",
|
||||||
"@react-native/normalize-colors": "^0.74.1",
|
"@react-native/normalize-colors": "^0.74.1",
|
||||||
@@ -9723,7 +9702,6 @@
|
|||||||
"version": "0.14.2",
|
"version": "0.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
|
||||||
"integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
|
"integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -10914,7 +10892,6 @@
|
|||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -11104,7 +11081,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
|
|||||||
Reference in new issue
Block a user