From 17ed120ce87f63d9feb3b0abbb6295b87f7154dd Mon Sep 17 00:00:00 2001 From: mudabbir-ahmad Date: Thu, 2 Apr 2026 22:08:07 +0100 Subject: [PATCH] Add new screens for game management, team management, and cache handling; implement caching logic and session management --- App.js | 81 ++ DB/DB-instructions.md | 134 --- README.md | 142 +-- app.json | 42 +- app/API/API-ReadMe.md | 18 - app/API/API.js | 249 ----- app/Controller/LoginLogic.js | 32 - app/Model/DbController.js | 526 ---------- app/Model/GroupModel.js | 34 - app/Model/SessionStore.js | 45 - app/Model/TeamModel.js | 26 - app/Model/UserModel.js | 30 - app/Screens/AdminHubPage.js | 96 -- app/Screens/CreateGamePage.js | 34 - app/Screens/GameLobbyPage.js | 128 --- app/Screens/GameTypePage.js | 60 -- app/Screens/JoinPrivateGamePage.js | 76 -- app/Screens/LoginPage.js | 114 --- app/Screens/ManageGamePage.js | 51 - app/Screens/ManagePrivateSubtaskPage.js | 124 --- app/Screens/ManageSubgroupsPage.js | 174 ---- app/Screens/MapPage.js | 40 - app/Screens/TeamPage.js | 40 - app/ViewModel/useAdminViewModel.js | 29 - app/ViewModel/useAuthViewModel.js | 71 -- app/ViewModel/useGameTypeViewModel.js | 23 - app/ViewModel/useLobbyViewModel.js | 51 - .../useManagePrivateSubtaskViewModel.js | 97 -- app/ViewModel/useMapViewModel.js | 30 - app/ViewModel/usePrivateGameViewModel.js | 31 - app/ViewModel/useSubgroupAdminViewModel.js | 82 -- app/ViewModel/useTeamViewModel.js | 33 - app/_layout.tsx | 36 - app/components/AdminFooterNavbar.js | 50 - app/components/MemberFooterNavbar.js | 54 - app/components/ScreenHeader.js | 108 -- app/components/UI/EntityCard.jsx | 64 -- app/components/UI/SearchBar.jsx | 34 - eslint.config.js | 1 - package-lock.json | 931 ++---------------- package.json | 20 +- src/components/UI/Button.js | 38 + .../gameplay/AdminCacheEditorView.js | 46 + src/components/gameplay/ClaimTimerView.js | 48 + src/components/gameplay/PlayerMapView.js | 46 + src/components/layout/BottomNavbar.js | 74 ++ src/components/layout/Screen.js | 53 + src/components/layout/TopNavbar.js | 90 ++ src/components/screens/AdminHubScreen.js | 18 + src/components/screens/CreateGameScreen.js | 18 + src/components/screens/GameLobbyScreen.js | 18 + src/components/screens/GameTypeScreen.js | 50 + .../screens/JoinPrivateGameScreen.js | 30 + src/components/screens/LeaderboardScreen.js | 19 + src/components/screens/LoginScreen.js | 110 +++ src/components/screens/ManageGameScreen.js | 168 ++++ .../screens/ManagePrivateGameScreen.js | 20 + .../screens/ManageSubgroupsScreen.js | 18 + src/components/screens/MapScreen.js | 183 ++++ .../components/screens/RegisterScreen.js | 119 ++- src/components/screens/TeamScreen.js | 18 + src/hooks/DbController.js | 495 ++++++++++ src/hooks/SessionStore.js | 41 + src/hooks/dbLink.js | 24 + src/hooks/useAuthHook.js | 117 +++ src/hooks/useGameHook.js | 72 ++ src/hooks/usePlayerGame.js | 38 + src/models/Cache.js | 17 + src/utils/geoMath.js | 18 + tsconfig.json | 17 - 70 files changed, 2172 insertions(+), 3892 deletions(-) create mode 100644 App.js delete mode 100644 DB/DB-instructions.md delete mode 100644 app/API/API-ReadMe.md delete mode 100644 app/API/API.js delete mode 100644 app/Controller/LoginLogic.js delete mode 100644 app/Model/DbController.js delete mode 100644 app/Model/GroupModel.js delete mode 100644 app/Model/SessionStore.js delete mode 100644 app/Model/TeamModel.js delete mode 100644 app/Model/UserModel.js delete mode 100644 app/Screens/AdminHubPage.js delete mode 100644 app/Screens/CreateGamePage.js delete mode 100644 app/Screens/GameLobbyPage.js delete mode 100644 app/Screens/GameTypePage.js delete mode 100644 app/Screens/JoinPrivateGamePage.js delete mode 100644 app/Screens/LoginPage.js delete mode 100644 app/Screens/ManageGamePage.js delete mode 100644 app/Screens/ManagePrivateSubtaskPage.js delete mode 100644 app/Screens/ManageSubgroupsPage.js delete mode 100644 app/Screens/MapPage.js delete mode 100644 app/Screens/TeamPage.js delete mode 100644 app/ViewModel/useAdminViewModel.js delete mode 100644 app/ViewModel/useAuthViewModel.js delete mode 100644 app/ViewModel/useGameTypeViewModel.js delete mode 100644 app/ViewModel/useLobbyViewModel.js delete mode 100644 app/ViewModel/useManagePrivateSubtaskViewModel.js delete mode 100644 app/ViewModel/useMapViewModel.js delete mode 100644 app/ViewModel/usePrivateGameViewModel.js delete mode 100644 app/ViewModel/useSubgroupAdminViewModel.js delete mode 100644 app/ViewModel/useTeamViewModel.js delete mode 100644 app/_layout.tsx delete mode 100644 app/components/AdminFooterNavbar.js delete mode 100644 app/components/MemberFooterNavbar.js delete mode 100644 app/components/ScreenHeader.js delete mode 100644 app/components/UI/EntityCard.jsx delete mode 100644 app/components/UI/SearchBar.jsx create mode 100644 src/components/UI/Button.js create mode 100644 src/components/gameplay/AdminCacheEditorView.js create mode 100644 src/components/gameplay/ClaimTimerView.js create mode 100644 src/components/gameplay/PlayerMapView.js create mode 100644 src/components/layout/BottomNavbar.js create mode 100644 src/components/layout/Screen.js create mode 100644 src/components/layout/TopNavbar.js create mode 100644 src/components/screens/AdminHubScreen.js create mode 100644 src/components/screens/CreateGameScreen.js create mode 100644 src/components/screens/GameLobbyScreen.js create mode 100644 src/components/screens/GameTypeScreen.js create mode 100644 src/components/screens/JoinPrivateGameScreen.js create mode 100644 src/components/screens/LeaderboardScreen.js create mode 100644 src/components/screens/LoginScreen.js create mode 100644 src/components/screens/ManageGameScreen.js create mode 100644 src/components/screens/ManagePrivateGameScreen.js create mode 100644 src/components/screens/ManageSubgroupsScreen.js create mode 100644 src/components/screens/MapScreen.js rename app/Screens/RegisterPage.js => src/components/screens/RegisterScreen.js (55%) create mode 100644 src/components/screens/TeamScreen.js create mode 100644 src/hooks/DbController.js create mode 100644 src/hooks/SessionStore.js create mode 100644 src/hooks/dbLink.js create mode 100644 src/hooks/useAuthHook.js create mode 100644 src/hooks/useGameHook.js create mode 100644 src/hooks/usePlayerGame.js create mode 100644 src/models/Cache.js create mode 100644 src/utils/geoMath.js delete mode 100644 tsconfig.json diff --git a/App.js b/App.js new file mode 100644 index 0000000..0ae59de --- /dev/null +++ b/App.js @@ -0,0 +1,81 @@ +import {NavigationContainer} from "@react-navigation/native"; +import {createNativeStackNavigator} from "@react-navigation/native-stack"; +import LoginScreen from "./src/components/screens/LoginScreen"; +import RegisterScreen from "./src/components/screens/RegisterScreen"; +import GameTypeScreen from "./src/components/screens/GameTypeScreen"; +import JoinPrivateGameScreen from "./src/components/screens/JoinPrivateGameScreen"; +import ManagePrivateGameScreen from "./src/components/screens/ManagePrivateGameScreen"; +import CreateGameScreen from "./src/components/screens/CreateGameScreen"; +import ManageGameScreen from "./src/components/screens/ManageGameScreen"; +import GameLobbyScreen from "./src/components/screens/GameLobbyScreen"; +import TeamScreen from "./src/components/screens/TeamScreen"; +import MapScreen from "./src/components/screens/MapScreen"; +import LeaderboardScreen from "./src/components/screens/LeaderboardScreen"; +import AdminHubScreen from "./src/components/screens/AdminHubScreen"; +import ManageSubgroupsScreen from "./src/components/screens/ManageSubgroupsScreen"; + +const Stack = createNativeStackNavigator(); + +const AuthStack = () => ( + + + + +); + +const GameStack = () => ( + + + + + + + + + + + +); + +const AdminStack = () => ( + + + + + + + + + +); + +export const App = () => { + return ( + + + + + + + + ); +}; + +export default App; + diff --git a/DB/DB-instructions.md b/DB/DB-instructions.md deleted file mode 100644 index b5fefdd..0000000 --- a/DB/DB-instructions.md +++ /dev/null @@ -1,134 +0,0 @@ -# Database Structure & Game Rules - -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`, `isBusiness`, `IsAcceptedAdmin`). -- `groups.json`: top-level game groups (`Gid`), admin approvals, group settings, creator metadata (`CreatedByUid`, `IsBusinessGroup`), and subgroup limits (`MaxMemberSubgroups`). -- `subgroups.json`: subgroup definitions within each group (`SGid` scoped by `Gid`), join codes, cache trigger defaults, and game status (`IsGameStarted`). -- `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; `1+` are member subgroups). -- `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. - -## User Types: Individual vs Business - -### Individual Users (`isBusiness: false`) - -**Private Game Creation Rules:** -- Individuals can create **one private game per account**. -- Auto-creates: - - **Subgroup 0**: Admin subgroup (empty on creation). - - **Subgroup 1**: Members subgroup (individual joins as member, not admin). -- `MaxMemberSubgroups` is set to `1` (no additional subgroups can be created). -- Individual has `SGid = 1` and `IsAcceptedAdmin = false`. -- If an individual tries to create a game again, returns the existing game. - -**Team Participation:** -- Individuals can create and join teams **only in subgroup 1**. -- Cannot access admin functions or view Admin Hub. -- Game management is simplified: "Manage Game" screen instead of "Admin Hub". - -### Business/School Users (`isBusiness: true`) - -**Private Game Creation Rules:** -- Businesses can create **unlimited private games** (tracked per creator `CreatedByUid`). -- Auto-creates: - - **Subgroup 0**: Admin subgroup (business becomes member). - - **No automatic subgroups 1+** (must be created manually). -- `MaxMemberSubgroups` is set to `999` (supports up to 999 subgroups). -- Business creator has `SGid = 0` and `IsAcceptedAdmin = true` (immediately accepted). - -**Admin Approval Flow:** -- When a new admin joins via "Join as Admin for Private Game": - - If an active admin already exists (`hasActiveAdmin` check for SGid 0, IsAcceptedAdmin true): - - Request added to `admin-waitlist.json` with status `pending`. - - User is notified: "Admin access request submitted. You will see admin tools after approval." - - If **no active admin exists**: - - User is auto-promoted to accepted admin immediately. - - `IsAcceptedAdmin` is set to `true`. - -**Team Participation & Admin Restrictions:** -- **Admins cannot create or join teams** (checked via `canJoinTeamForSubgroup` logic). -- Admins in subgroup 0 see a warning in Game Lobby: "Admins cannot participate in teams. Manage your game from the Admin Hub instead." -- Team controls are hidden for admin users. - -## Subgroup Visibility & Management - -### Manage Subgroups Page - -- **Only business users can access** (individuals see an informational message). -- **Only displays subgroups with `SGid > 0`** (member subgroups). - - Subgroup 0 (admin) is hidden to avoid confusion. -- **Features:** - - Search bar to filter by subgroup name or ID. - - Card-based display showing: - - Bold subgroup name - - `ID: [SGid]` - - `Status: [Active|Inactive]` (based on `IsGameStarted` flag) - - `Teams: [true|false]` - - Create form at bottom for adding new subgroups (up to `MaxMemberSubgroups` limit). - -### Visibility API - -- `getVisibleSubgroups(gid)` returns only subgroups where `SGid > 0`. -- Used by ManageSubgroupsPage for filtering. -- Maintains admin-only (subgroup 0) separation in UI. - -## Admin Logic - -- Group creator is auto-set as admin (`SGid = 0`) only if **business account**. -- Individual creators are set to subgroup 1 (members) and are never admins. -- Admins cannot create/join teams (blocked by `canJoinTeamForSubgroup` check). -- 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` - -## Summary: Game Flow Decision Tree - -``` -User Registration -├─ Individual (isBusiness = false) -│ └─ Login → "Manage Private Game" -│ ├─ Has created game before? -│ │ ├─ Yes → "Manage Game" screen (team manager) -│ │ └─ No → "Create Game" screen → creates game with subgroups 0 & 1 -│ └─ Never sees Admin Hub -│ -└─ Business (isBusiness = true) - └─ Login → "Manage Private Game" - ├─ "Create New Private Game" → creates game with subgroup 0 only - └─ "Join as Admin for Private Game" → requests admin access - ├─ Active admin exists? - │ ├─ Yes → queued in admin-waitlist (pending) - │ └─ No → auto-promoted (accepted) - └─ Once accepted → Admin Hub access - ├─ Create a Game - ├─ Manage Existing Game (blocked from teams) - └─ Manage Subgroups (create/search member subgroups) -``` - diff --git a/README.md b/README.md index 7a02635..3886e14 100644 --- a/README.md +++ b/README.md @@ -1,132 +1,26 @@ -# Welcome to the mobile application development repository for our Treasure Hunt Game! This project is built using React Native and Expo. +# MAD Treasure Hunt -## Get started +Treasure Hunt mobile app built with React Native and Expo. -1. Install dependencies +## Run - ```bash - npm i - ``` +```bash +npm install +npm run lint +expo start --android +``` -2. Start the app +## Structure - ```bash - expo start --android - ``` +- `App.js`: app navigation entry. +- `src/components/layout`: shared screen wrapper. +- `src/components/UI`: shared UI components. +- `src/components/screens`: app screens. +- `src/hooks`: app state, local DB link, and feature hooks. +- `DB`: local JSON data files. +## Notes - -# 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`, `isBusiness`, `IsAcceptedAdmin`). -- `groups.json`: top-level game groups (`Gid`), admin approvals, group settings, creator metadata (`CreatedByUid`, `IsBusinessGroup`), and subgroup limits (`MaxMemberSubgroups`). -- `subgroups.json`: subgroup definitions within each group (`SGid` scoped by `Gid`), join codes, cache trigger defaults, and game status (`IsGameStarted`). -- `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; `1+` are member subgroups). -- `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. - -## User Types and Private Game Rules - -### Individual Users (`isBusiness: false`) - -- **Private Game Creation**: Individuals can create **one** private game per account. - - Auto-creates **subgroup 0** (admins) and **subgroup 1** (members/self). - - `MaxMemberSubgroups` is set to `1` (cannot create additional subgroups). - - Individual becomes a **member** of subgroup 1, not an admin. - - Cannot view "Admin Hub" options; instead see "Manage Game" UI for active team management. - -- **Team Participation**: Individuals can create and join teams within their subgroup 1. - - Admins (those in subgroup 0) are blocked from team participation. - -### Business/School Users (`isBusiness: true`) - -- **Private Game Creation**: Businesses can create private games with flexible subgroup management. - - Auto-creates **subgroup 0** (admins) only; no auto-member subgroup. - - `MaxMemberSubgroups` is set to `999` (can create up to 999 subgroups). - - Business creator becomes an **accepted admin** of subgroup 0 immediately. - -- **Admin Participation**: Business admins (SGid 0, IsAcceptedAdmin true) **cannot participate in teams**. - - Game Lobby shows warning message and hides team creation/join controls. - - Admins must manage subgroups and games from Admin Hub and Manage Subgroups pages. - -- **Admin Approval Flow**: New admins joining a business group go into `admin-waitlist.json` if an active admin already exists. - - Auto-promoted if no active admin exists. - - Promoted admins gain accepted admin status. - -## Subgroup Visibility - -- **Manage Subgroups Page**: Only displays subgroups with `SGid > 0` (member subgroups). - - Subgroup 0 (admins) is hidden in the UI to avoid confusion. - - Features search by name/ID and card-based display with status and team info. - - Only visible to business users; individual users see an informational message. - -## Admin Logic - -- Group creator is auto-set as admin (`SGid = 0`) if business account. -- 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. -- Admins cannot create or join teams; they manage the game and subgroups. - -## Game Flow - -### Login / Registration - -1. User registers as Individual or Business/School. -2. Session stores `isBusiness` and `IsAcceptedAdmin` flags. - -### Individual Private Game Flow - -1. Individual selects "Manage Private Game" option. -2. Check if they have an existing private game: - - **If existing**: Navigate to "Manage Game" screen → "Open Team Manager" → join/create team. - - **If new**: Navigate to "Create Game" screen → create game → then to "Manage Game". -3. Individual can only manage their single team in subgroup 1. -4. Cannot access Admin Hub or Subgroup management. - -### Business Private Game Flow - -1. Business user selects "Manage Private Game" option. -2. Presented with: - - **Create New Private Game**: Creates a game with admin subgroup 0 only. - - **Join as Admin for Private Game**: Submit request to join an existing business group. -3. Once admin (accepted or auto-promoted), access Admin Hub: - - Create a Game - - Manage Existing Game (team/lobby controls) - - Manage Subgroups (search, filter, create member subgroups) -4. Cannot create/join teams in Game Lobby (shows warning). - -### Team Management - -- Teams can only be created/joined in **non-admin subgroups** (`SGid > 0`). -- Admin users (SGid 0 + IsAcceptedAdmin) are blocked from team participation. -- Teams are scoped per subgroup. - -## Timestamp Format - -- `CreatedAt` values are stored at minute precision: - - format: `YYYY-MM-DDTHH:mmZ` - - example: `2026-03-17T14:42Z` +- The app uses local JSON data from `DB` through `src/hooks/DbController.js`. +- Navigation is handled with React Navigation native stack. diff --git a/app.json b/app.json index 4cedf42..be4b335 100644 --- a/app.json +++ b/app.json @@ -2,48 +2,28 @@ "expo": { "name": "MAD_Treasure_Hunt_App", "slug": "MAD_Treasure_Hunt_App", - "version": "1.0.0", + "version": "1.0.0", "orientation": "portrait", "icon": "./assets/images/icon.png", - "scheme": "madtreasurehuntapp", - "userInterfaceStyle": "automatic", + "userInterfaceStyle": "light", "newArchEnabled": true, + "splash": { + "image": "./assets/images/splash-icon.png", + "resizeMode": "contain", + "backgroundColor": "#ffffff" + }, "ios": { "supportsTablet": true }, "android": { "adaptiveIcon": { - "backgroundColor": "#E6F4FE", - "foregroundImage": "./assets/images/android-icon-foreground.png", - "backgroundImage": "./assets/images/android-icon-background.png", - "monochromeImage": "./assets/images/android-icon-monochrome.png" + "foregroundImage": "./assets/images/android-icon-monochrome.png", + "backgroundColor": "#ffffff" }, - "edgeToEdgeEnabled": true, - "predictiveBackGestureEnabled": false, - "package": "com.bobthebob.MAD_Treasure_Hunt_App" + "edgeToEdgeEnabled": true }, "web": { - "output": "static", - "favicon": "./assets/images/favicon.png" - }, - "plugins": [ - "expo-router", - [ - "expo-splash-screen", - { - "image": "./assets/images/splash-icon.png", - "imageWidth": 200, - "resizeMode": "contain", - "backgroundColor": "#ffffff", - "dark": { - "backgroundColor": "#000000" - } - } - ] - ], - "experiments": { - "typedRoutes": true, - "reactCompiler": true + "favicon": "./assets/favicon.png" } } } diff --git a/app/API/API-ReadMe.md b/app/API/API-ReadMe.md deleted file mode 100644 index 06e887d..0000000 --- a/app/API/API-ReadMe.md +++ /dev/null @@ -1,18 +0,0 @@ -# ReadMe for the API endpoint - -### This page is for my project partner to understand the API endpoint, how to use it and also when to use it. - - -The database will first be made locally with regular SQL. - -Once the database is initialised and fully created locally, we can move the logic [refactor] it to use the API endpoint that will be created -api endpoint will be used here to fetch the data from the database and then display it on the frontend. -Same logic applies so minimal change in the code, for now we'll have the api endpoint point to the local database. -Therefore, still a need for the API.js file to be created and used in the frontend to fetch the data from the database and display it on the frontend. -To refactor its just going to be a simple replace. - -### API Endpoint -``` (Endpoint Link/URL once it's made.) ``` - -### How to use it -[Will be populated once there is data for me to provide here.] \ No newline at end of file diff --git a/app/API/API.js b/app/API/API.js deleted file mode 100644 index a608f3c..0000000 --- a/app/API/API.js +++ /dev/null @@ -1,249 +0,0 @@ -import dbController from '../Model/DbController'; - -const apiConfig = { - useLocalDb: true, - baseUrl: 'http://localhost:3000', -}; - -const setApiConfig = (nextConfig = {}) => { - Object.assign(apiConfig, nextConfig); -}; - -const isSameId = (item, id) => - item.id?.toString() === id - || item.Uid?.toString() === id - || item.Gid?.toString() === id - || item.SGid?.toString() === id - || item.Tid?.toString() === id; - -const normalize = (endpoint) => endpoint.replace(/^\/+/, '').split('/'); - -const localGet = (endpoint) => { - const [resource, id] = normalize(endpoint); - const state = dbController.getState(); - - if (!resource) { - return state; - } - if (!state[resource]) { - throw new Error(`Resource not found: ${resource}`); - } - if (!id) { - return state[resource]; - } - return state[resource].find((item) => isSameId(item, id)) || null; -}; - -const localPost = (endpoint, payload) => { - const [resource] = normalize(endpoint); - - if (resource === 'auth' && payload.action === 'login') { - return dbController.login(payload.data); - } - if (resource === 'auth' && payload.action === 'register') { - return dbController.register(payload.data); - } - if (resource === 'groups' && payload.action === 'joinPrivate') { - return dbController.joinPrivateGame(payload.data); - } - if (resource === 'groups' && payload.action === 'joinAsAdmin') { - return dbController.joinAsAdmin(payload.data); - } - if (resource === 'groups' && payload.action === 'createPrivate') { - return dbController.createPrivateGame(payload.data); - } - if (resource === 'groups' && payload.action === 'requestAdmin') { - return dbController.requestAdminApproval(payload.data); - } - if (resource === 'groups' && payload.action === 'approveAdmin') { - return dbController.approveAdmin(payload.data); - } - if (resource === 'groups' && payload.action === 'createdByUser') { - return dbController.getCreatedPrivateGameByUser(payload.data.userId); - } - 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; -}; - -const localPut = (endpoint, payload) => { - const [resource, id] = normalize(endpoint); - const state = dbController.getState(); - if (!state[resource]) { - throw new Error(`Resource not found: ${resource}`); - } - const index = state[resource].findIndex((item) => - isSameId(item, id), - ); - if (index < 0) { - throw new Error('Record not found'); - } - state[resource][index] = { ...state[resource][index], ...payload }; - return state[resource][index]; -}; - -const localDelete = (endpoint) => { - const [resource, id] = normalize(endpoint); - const state = dbController.getState(); - if (!state[resource]) { - throw new Error(`Resource not found: ${resource}`); - } - const index = state[resource].findIndex((item) => - isSameId(item, id), - ); - if (index < 0) { - return { deleted: false }; - } - state[resource].splice(index, 1); - return { deleted: true }; -}; - -const remoteRequest = async (endpoint, options = {}) => { - const response = await fetch(`${apiConfig.baseUrl}/${endpoint.replace(/^\/+/, '')}`, { - ...options, - headers: { - 'Content-Type': 'application/json', - ...(options.headers || {}), - }, - }); - if (!response.ok) { - throw new Error(`API error: ${response.status}`); - } - return response.json(); -}; - -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 getCreatedPrivateGame = async (userId) => APIPost('groups', { action: 'createdByUser', data: { userId } }); -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 getVisibleSubgroups = async (gid) => { - if (apiConfig.useLocalDb) { - return dbController.getVisibleSubgroups(gid); - } - return APIFetch(`groups/${gid}/subgroups?visibleOnly=true`); -}; -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, - getCreatedPrivateGame, - requestAdminAccess, - approveAdminAccess, - createSubgroup, - getSubgroups, - getVisibleSubgroups, - updateSubgroupCache, - updateGroupSettings, - createTeam, - joinTeam, - getLobby, - getTeam, - getMapPoints, -}; - -// Default export to satisfy Expo Router route file requirement when the folder is scanned. -// This module primarily provides named exports; returning null as a component is safe -// and prevents the router warning about missing default export. -export default function APIPlaceholder() { - return null; -} - diff --git a/app/Controller/LoginLogic.js b/app/Controller/LoginLogic.js deleted file mode 100644 index ceb2b83..0000000 --- a/app/Controller/LoginLogic.js +++ /dev/null @@ -1,32 +0,0 @@ -import { useState } from 'react'; -import { useNavigation } from '@react-navigation/native'; - -const useLoginLogic = () => { - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const navigation = useNavigation(); - - const handleLogin = async () => { - if (!email || !password) { - return false; - } - - if (email.trim().toLowerCase().includes('admin')) { - navigation.navigate('(routes)/admin-hub'); - return true; - } - - navigation.navigate('(routes)/game-type'); - return true; - }; - - return { - email, - password, - setEmail, - setPassword, - handleLogin, - }; -}; - -export default useLoginLogic; diff --git a/app/Model/DbController.js b/app/Model/DbController.js deleted file mode 100644 index e179919..0000000 --- a/app/Model/DbController.js +++ /dev/null @@ -1,526 +0,0 @@ -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; - } - - getCreatedPrivateGameByUser(uid) { - return this.db.groups.find( - (group) => group.CreatedByUid === Number(uid) && group.GameType === 'private', - ) || 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 existing = this.getCreatedPrivateGameByUser(user.Uid); - if (existing) { - return existing; - } - - const gid = this.nextId('group'); - const isBusinessGroup = Boolean(isBusiness); - const group = new GroupModel({ - Gid: gid, - GroupName: groupName, - BusinessOrSchoolName: businessOrSchoolName || groupName, - GameType: gameType, - AdminJoinCode: this.generateUniqueJoinCode(this.db.groups, 'AdminJoinCode'), - CreatedByUid: user.Uid, - MaxMemberSubgroups: isBusinessGroup ? 999 : 1, - TeamsEnabled: true, - IsBusinessGroup: isBusinessGroup, - ApprovedAdmins: isBusinessGroup ? [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, - IsGameStarted: false, - CacheTriggerMeters: 20, - }); - - if (!isBusinessGroup) { - this.createSubgroup({ gid: group.Gid, subgroupName: 'Members', cacheTriggerMeters: 20 }); - } - - const ownerSgid = isBusinessGroup ? 0 : 1; - const ownerIsAcceptedAdmin = isBusinessGroup; - - this.ensureMembership(user.Uid, group.Gid, ownerSgid, ownerIsAcceptedAdmin); - user.Gid = group.Gid; - user.SGid = ownerSgid; - user.IsAcceptedAdmin = ownerIsAcceptedAdmin; - - 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, - IsGameStarted: false, - CacheTriggerMeters: cacheTriggerMeters, - }; - - this.db.subgroups.push(subgroup); - return subgroup; - } - - getSubgroups(gid) { - return this.db.subgroups.filter((subgroup) => subgroup.Gid === Number(gid)); - } - - getVisibleSubgroups(gid) { - return this.getSubgroups(gid).filter((subgroup) => Number(subgroup.SGid) > 0); - } - - updateSubgroupCacheSettings({ gid, sgid, triggerMeters }) { - const subgroup = this.db.subgroups.find( - (entry) => entry.Gid === Number(gid) && entry.SGid === Number(sgid), - ); - if (!subgroup) { - throw new Error('Subgroup not found'); - } - subgroup.CacheTriggerMeters = Number(triggerMeters); - return subgroup; - } - - createTeam({ userId, gid, teamName }) { - const user = this.getUserById(Number(userId)); - if (!user) { - throw new Error('User not found'); - } - - const group = this.getGroupById(Number(gid)); - if (!group) { - throw new Error('Group not found'); - } - if (!this.canJoinTeamForSubgroup(group, user.SGid || 0)) { - throw new Error('Teams are disabled or user is in admin subgroup'); - } - - const tid = this.nextId('team'); - const team = new TeamModel({ - Tid: tid, - TeamName: teamName, - Gid: group.Gid, - JoinCode: this.generateUniqueJoinCode(this.db.teams, 'JoinCode'), - CreatorUid: user.Uid, - SGid: user.SGid, - CreatedAt: this.toMinuteIso(), - }); - - this.db.teams.push(team.toJSON()); - this.db.team_members.push({ - Tid: team.Tid, - Uid: user.Uid, - IsTeamCreator: true, - }); - - user.TGid = team.Tid; - return team.toJSON(); - } - - joinTeamByCode({ userId, teamCode }) { - const user = this.getUserById(Number(userId)); - const team = this.db.teams.find((t) => t.JoinCode === teamCode.toUpperCase()); - if (!user || !team) { - throw new Error('Invalid user or team code'); - } - - const group = this.getGroupById(team.Gid); - if (!group || !this.canJoinTeamForSubgroup(group, user.SGid || 0)) { - throw new Error('Teams are disabled or user is in admin subgroup'); - } - - if (user.Gid !== team.Gid) { - throw new Error('User is not in this game group'); - } - - const alreadyInTeam = this.db.team_members.some((tm) => tm.Tid === team.Tid && tm.Uid === user.Uid); - if (!alreadyInTeam) { - this.db.team_members.push({ Tid: team.Tid, Uid: user.Uid, IsTeamCreator: false }); - } - - user.TGid = team.Tid; - return team; - } - - getLobby(gid) { - const group = this.getGroupById(Number(gid)); - if (!group) { - return null; - } - - const members = this.db.subgroup_memberships - .filter((membership) => membership.Gid === group.Gid) - .map((membership) => this.getUserById(membership.Uid)) - .filter(Boolean); - - return { - group, - members, - subgroups: this.getSubgroups(group.Gid), - teams: this.db.teams.filter((team) => team.Gid === group.Gid), - }; - } - - getTeam(tid) { - const team = this.db.teams.find((entry) => entry.Tid === Number(tid)); - if (!team) { - return null; - } - const members = this.db.team_members - .filter((member) => member.Tid === team.Tid) - .map((member) => this.getUserById(member.Uid)) - .filter(Boolean); - return { team, members }; - } - - getMapPoints(gid, sgid = null) { - const gameData = this.db.game_data.find((entry) => entry.Gid === Number(gid)); - if (!gameData) { - return []; - } - if (sgid === null || sgid === undefined) { - return gameData.GeoCaches; - } - return gameData.GeoCaches.filter((cache) => cache.SGid === Number(sgid)); - } - - updateGroupSettings({ gid, teamsEnabled }) { - const group = this.getGroupById(Number(gid)); - if (!group) { - throw new Error('Group not found'); - } - group.TeamsEnabled = Boolean(teamsEnabled); - return group; - } - - getState() { - return this.db; - } -} - -const dbController = new DbController(); - -export { DbController }; -export default dbController; - diff --git a/app/Model/GroupModel.js b/app/Model/GroupModel.js deleted file mode 100644 index a0f5c35..0000000 --- a/app/Model/GroupModel.js +++ /dev/null @@ -1,34 +0,0 @@ -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.IsBusinessGroup = payload.IsBusinessGroup !== undefined ? Boolean(payload.IsBusinessGroup) : true; - 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, - IsBusinessGroup: this.IsBusinessGroup, - ApprovedAdmins: this.ApprovedAdmins, - CreatedAt: this.CreatedAt, - }; - } -} - -export default GroupModel; - diff --git a/app/Model/SessionStore.js b/app/Model/SessionStore.js deleted file mode 100644 index 9ec9486..0000000 --- a/app/Model/SessionStore.js +++ /dev/null @@ -1,45 +0,0 @@ -const session = { - currentUid: null, - currentGid: null, - currentSGid: null, - currentTGid: null, - currentIsBusiness: null, - currentIsAcceptedAdmin: false, -}; - -const setSessionUser = (user) => { - session.currentUid = user?.Uid ?? null; - session.currentGid = user?.Gid ?? null; - session.currentSGid = user?.SGid ?? null; - session.currentTGid = user?.TGid ?? null; - session.currentIsBusiness = user?.isBusiness ?? null; - session.currentIsAcceptedAdmin = Boolean(user?.IsAcceptedAdmin); -}; - -const setSessionGroup = (gid, sgid = null) => { - session.currentGid = gid ?? null; - if (sgid !== null && sgid !== undefined) { - session.currentSGid = sgid; - } -}; - -const setSessionMembership = ({ gid = null, sgid = null, isAcceptedAdmin = false } = {}) => { - session.currentGid = gid; - session.currentSGid = sgid; - session.currentIsAcceptedAdmin = Boolean(isAcceptedAdmin); -}; - -const setSessionTeam = (tgid) => { - session.currentTGid = tgid ?? null; -}; - -const getSession = () => ({ ...session }); - -export { setSessionUser, setSessionGroup, setSessionMembership, setSessionTeam, getSession }; - -// Default placeholder export to avoid Expo Router treating this non-component file as a route without a default. -export default function SessionStorePlaceholder() { - return null; -} - - diff --git a/app/Model/TeamModel.js b/app/Model/TeamModel.js deleted file mode 100644 index a22618e..0000000 --- a/app/Model/TeamModel.js +++ /dev/null @@ -1,26 +0,0 @@ -class TeamModel { - constructor(payload) { - this.Tid = payload.Tid; - this.TeamName = payload.TeamName; - this.Gid = payload.Gid; - this.JoinCode = payload.JoinCode; - this.CreatorUid = payload.CreatorUid; - this.SGid = payload.SGid; - this.CreatedAt = payload.CreatedAt; - } - - toJSON() { - return { - Tid: this.Tid, - TeamName: this.TeamName, - Gid: this.Gid, - JoinCode: this.JoinCode, - CreatorUid: this.CreatorUid, - SGid: this.SGid, - CreatedAt: this.CreatedAt, - }; - } -} - -export default TeamModel; - diff --git a/app/Model/UserModel.js b/app/Model/UserModel.js deleted file mode 100644 index 42888d2..0000000 --- a/app/Model/UserModel.js +++ /dev/null @@ -1,30 +0,0 @@ -class UserModel { - constructor(payload) { - this.Uid = payload.Uid; - this.username = payload.username; - this.email = payload.email; - this.passwordHash = payload.passwordHash; - this.isBusiness = Boolean(payload.isBusiness); - this.IsAcceptedAdmin = Boolean(payload.IsAcceptedAdmin); - this.Gid = payload.Gid ?? null; - this.SGid = payload.SGid ?? null; - this.TGid = payload.TGid ?? null; - } - - toJSON() { - return { - Uid: this.Uid, - username: this.username, - email: this.email, - passwordHash: this.passwordHash, - isBusiness: this.isBusiness, - IsAcceptedAdmin: this.IsAcceptedAdmin, - Gid: this.Gid, - SGid: this.SGid, - TGid: this.TGid, - }; - } -} - -export default UserModel; - diff --git a/app/Screens/AdminHubPage.js b/app/Screens/AdminHubPage.js deleted file mode 100644 index e53f1be..0000000 --- a/app/Screens/AdminHubPage.js +++ /dev/null @@ -1,96 +0,0 @@ -import React from 'react'; -import {Pressable, ScrollView, StyleSheet, Text, View} from 'react-native'; -import {useNavigation} from '@react-navigation/native'; -import AdminFooterNavbar from '../components/AdminFooterNavbar'; -import useAdminViewModel from '../ViewModel/useAdminViewModel'; -import ScreenHeader from '../components/ScreenHeader'; -import {getSession} from '../Model/SessionStore'; - -const AdminHubPage = () => { - const navigation = useNavigation(); - const { currentIsBusiness } = getSession(); - 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('(routes)/game-lobby'); - }; - - if (!currentIsBusiness) { - return ( - - - - - As an individual, your game is managed automatically. You can view and manage your active team from the Game Lobby. - - navigation.navigate('(routes)/game-lobby')}> - Go to Game Lobby - - - - - ); - } - - return ( - - - - - - Create a Game - - navigation.navigate('(routes)/game-lobby')}> - Manage Existing Game - - navigation.navigate('(routes)/manage-subgroups')}> - Manage Subgroups - - - - - - ); -}; - -const styles = StyleSheet.create({ - wrapper: { - flex: 1, - backgroundColor: '#ffffff', - }, - container: { - flex: 1, - justifyContent: 'center', - padding: 20, - gap: 12, - }, - message: { - color: '#4b5563', - fontSize: 15, - lineHeight: 22, - marginBottom: 20, - }, - buttonList: { - flex: 1, - }, - button: { - backgroundColor: '#2563eb', - borderRadius: 8, - paddingVertical: 12, - alignItems: 'center', - marginBottom: 10, - }, - buttonText: { - color: '#ffffff', - fontWeight: '600', - }, -}); - -export default AdminHubPage; - diff --git a/app/Screens/CreateGamePage.js b/app/Screens/CreateGamePage.js deleted file mode 100644 index 0b880fb..0000000 --- a/app/Screens/CreateGamePage.js +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import {StyleSheet, Text, View} from 'react-native'; -import ScreenHeader from '../components/ScreenHeader'; - -const CreateGamePage = () => { - return ( - - - - Create game setup goes here. - - - ); -}; - -const styles = StyleSheet.create({ - wrapper: { - flex: 1, - backgroundColor: '#ffffff', - }, - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - padding: 20, - }, - body: { - color: '#4b5563', - fontSize: 15, - }, -}); - -export default CreateGamePage; - diff --git a/app/Screens/GameLobbyPage.js b/app/Screens/GameLobbyPage.js deleted file mode 100644 index f37a649..0000000 --- a/app/Screens/GameLobbyPage.js +++ /dev/null @@ -1,128 +0,0 @@ -import React, {useState} from 'react'; -import {Pressable, StyleSheet, Text, TextInput, View} from 'react-native'; -import {useNavigation} from '@react-navigation/native'; -import MemberFooterNavbar from '../components/MemberFooterNavbar'; -import useLobbyViewModel from '../ViewModel/useLobbyViewModel'; -import ScreenHeader from '../components/ScreenHeader'; -import {getSession} from '../Model/SessionStore'; - -const GameLobbyPage = () => { - const navigation = useNavigation(); - const { currentSGid, currentIsAcceptedAdmin } = getSession(); - const { teamCode, error, setTeamCode, createLobbyTeam, joinLobbyTeam } = useLobbyViewModel(); - const [hasTeam, setHasTeam] = useState(false); - - const isAdminUser = currentSGid === 0 && currentIsAcceptedAdmin; - - const handleCreateTeam = async () => { - const team = await createLobbyTeam({ - teamName: `Team-${Date.now().toString().slice(-4)}`, - }); - if (!team) { - return; - } - setHasTeam(true); - navigation.navigate('(routes)/team'); - }; - - const handleJoinTeam = async () => { - const team = await joinLobbyTeam(); - if (!team) { - return; - } - setHasTeam(true); - navigation.navigate('(routes)/team'); - }; - - return ( - - - - {isAdminUser && ( - - - Admins cannot participate in teams. Manage your game from the Admin Hub instead. - - - )} - {!isAdminUser && ( - <> - - - Join Team - - - Create Team - - - )} - navigation.navigate('(routes)/map')}> - Open Map - - {!!error && {error}} - - - - ); -}; - -const styles = StyleSheet.create({ - wrapper: { - flex: 1, - backgroundColor: '#ffffff', - }, - container: { - flex: 1, - justifyContent: 'center', - padding: 20, - gap: 12, - }, - 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', - }, - adminMessage: { - backgroundColor: '#fef3c7', - borderWidth: 1, - borderColor: '#fcd34d', - borderRadius: 8, - padding: 12, - marginBottom: 12, - }, - adminMessageText: { - color: '#92400e', - fontSize: 13, - fontWeight: '500', - }, -}); - -export default GameLobbyPage; diff --git a/app/Screens/GameTypePage.js b/app/Screens/GameTypePage.js deleted file mode 100644 index c342325..0000000 --- a/app/Screens/GameTypePage.js +++ /dev/null @@ -1,60 +0,0 @@ -import React from 'react'; -import { View, Text, Pressable, StyleSheet } from 'react-native'; -import { useNavigation } from '@react-navigation/native'; -import useGameTypeViewModel from '../ViewModel/useGameTypeViewModel'; -import ScreenHeader from '../components/ScreenHeader'; - -const GameTypePage = () => { - const navigation = useNavigation(); - const { gameTypes } = useGameTypeViewModel(); - - const navigateByType = (label) => { - if (label === 'Join Global Game') { - navigation.navigate('(routes)/game-lobby'); - return; - } - if (label === 'Join Private Game') { - navigation.navigate('(routes)/join-private-game'); - return; - } - navigation.navigate('(routes)/manage-private-subtask'); - }; - - return ( - - - - {gameTypes.map((label) => ( - navigateByType(label)}> - {label} - - ))} - - - ); -}; - -const styles = StyleSheet.create({ - wrapper: { - flex: 1, - backgroundColor: '#ffffff', - }, - container: { - flex: 1, - justifyContent: 'center', - padding: 20, - gap: 12, - }, - button: { - backgroundColor: '#2563eb', - borderRadius: 8, - paddingVertical: 12, - alignItems: 'center', - }, - buttonText: { - color: '#ffffff', - fontWeight: '600', - }, -}); - -export default GameTypePage; diff --git a/app/Screens/JoinPrivateGamePage.js b/app/Screens/JoinPrivateGamePage.js deleted file mode 100644 index 4525005..0000000 --- a/app/Screens/JoinPrivateGamePage.js +++ /dev/null @@ -1,76 +0,0 @@ -import React, { useState } from 'react'; -import { View, Text, TextInput, Pressable, StyleSheet } from 'react-native'; -import { useNavigation } from '@react-navigation/native'; -import usePrivateGameViewModel from '../ViewModel/usePrivateGameViewModel'; -import ScreenHeader from '../components/ScreenHeader'; - -const JoinPrivateGamePage = () => { - const navigation = useNavigation(); - const { joinCode, error, setJoinCode, joinWithCode } = usePrivateGameViewModel(); - const [submitting, setSubmitting] = useState(false); - - const handleJoinGame = async () => { - setSubmitting(true); - const group = await joinWithCode(); - setSubmitting(false); - if (!group) { - return; - } - navigation.navigate('(routes)/game-lobby'); - }; - - return ( - - - - - - {submitting ? 'Joining...' : 'Join Game'} - - {!!error && {error}} - - - ); -}; - -const styles = StyleSheet.create({ - wrapper: { - flex: 1, - backgroundColor: '#ffffff', - }, - container: { - flex: 1, - justifyContent: 'center', - padding: 20, - gap: 12, - }, - 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; diff --git a/app/Screens/LoginPage.js b/app/Screens/LoginPage.js deleted file mode 100644 index 12ecf67..0000000 --- a/app/Screens/LoginPage.js +++ /dev/null @@ -1,114 +0,0 @@ -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'; -import ScreenHeader from '../components/ScreenHeader'; - -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('(routes)/admin-hub'); - return; - } - navigation.navigate('(routes)/game-type'); - }; - - return ( - - - - - - - navigation.navigate('(routes)/register')}> - Register - - - {submitting ? 'Logging in...' : 'Login'} - - - {!!error && {error}} - - - ); -}; - -const styles = StyleSheet.create({ - wrapper: { - flex: 1, - backgroundColor: '#ffffff', - }, - container: { - flex: 1, - padding: 20, - justifyContent: 'center', - gap: 12, - }, - 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; - diff --git a/app/Screens/ManageGamePage.js b/app/Screens/ManageGamePage.js deleted file mode 100644 index 9b15b44..0000000 --- a/app/Screens/ManageGamePage.js +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react'; -import {Pressable, StyleSheet, Text, View} from 'react-native'; -import {useNavigation} from '@react-navigation/native'; -import ScreenHeader from '../components/ScreenHeader'; - -const ManageGamePage = () => { - const navigation = useNavigation(); - - return ( - - - - Manage your active team from here. - navigation.navigate('(routes)/team')}> - Open Team Manager - - - - ); -}; - -const styles = StyleSheet.create({ - wrapper: { - flex: 1, - backgroundColor: '#ffffff', - }, - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - gap: 12, - padding: 20, - }, - body: { - color: '#4b5563', - fontSize: 15, - }, - button: { - backgroundColor: '#2563eb', - borderRadius: 8, - paddingHorizontal: 16, - paddingVertical: 12, - }, - buttonText: { - color: '#ffffff', - fontWeight: '600', - }, -}); - -export default ManageGamePage; - diff --git a/app/Screens/ManagePrivateSubtaskPage.js b/app/Screens/ManagePrivateSubtaskPage.js deleted file mode 100644 index f7d91c6..0000000 --- a/app/Screens/ManagePrivateSubtaskPage.js +++ /dev/null @@ -1,124 +0,0 @@ -import React, {useState} from 'react'; -import {Pressable, StyleSheet, Text, TextInput, View} from 'react-native'; -import {useNavigation} from '@react-navigation/native'; -import useManagePrivateSubtaskViewModel from '../ViewModel/useManagePrivateSubtaskViewModel'; -import ScreenHeader from '../components/ScreenHeader'; - -const ManagePrivateSubtaskPage = () => { - const navigation = useNavigation(); - const { - businessOrSchoolName, - error, - isBusinessUser, - setBusinessOrSchoolName, - createNewPrivateGame, - resolveIndividualPrivateRoute, - joinAdminForPrivate, - } = useManagePrivateSubtaskViewModel(); - const [submitting, setSubmitting] = useState(false); - - const handleCreate = async () => { - setSubmitting(true); - if (!isBusinessUser) { - const nextRoute = await resolveIndividualPrivateRoute(); - setSubmitting(false); - if (nextRoute) { - navigation.navigate(nextRoute); - } - return; - } - - const group = await createNewPrivateGame(); - setSubmitting(false); - if (!group) { - return; - } - navigation.navigate('(routes)/admin-hub'); - }; - - const handleJoinAdmin = async () => { - setSubmitting(true); - const response = await joinAdminForPrivate(); - setSubmitting(false); - if (!response) { - return; - } - navigation.navigate('(routes)/admin-hub'); - }; - - return ( - - - - - {submitting ? 'Please wait...' : isBusinessUser ? 'Create New Private Game' : 'Create or Manage My Game'} - - {isBusinessUser && ( - - Business/School Name is only required for admin join. - - - Join as Admin for Private Game - - - )} - {!!error && {error}} - - - ); -}; - -const styles = StyleSheet.create({ - wrapper: { - flex: 1, - backgroundColor: '#ffffff', - }, - container: { - flex: 1, - justifyContent: 'center', - gap: 12, - padding: 20, - }, - adminJoinSection: { - marginTop: 14, - gap: 12, - }, - helperText: { - color: '#4b5563', - fontSize: 13, - }, - 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; - diff --git a/app/Screens/ManageSubgroupsPage.js b/app/Screens/ManageSubgroupsPage.js deleted file mode 100644 index d15d4bd..0000000 --- a/app/Screens/ManageSubgroupsPage.js +++ /dev/null @@ -1,174 +0,0 @@ -import React from 'react'; -import {Pressable, ScrollView, StyleSheet, Text, TextInput, View} from 'react-native'; -import useSubgroupAdminViewModel from '../ViewModel/useSubgroupAdminViewModel'; -import AdminFooterNavbar from '../components/AdminFooterNavbar'; -import ScreenHeader from '../components/ScreenHeader'; -import EntityCard from '../components/UI/EntityCard'; -import SearchBar from '../components/UI/SearchBar'; -import {getSession} from '../Model/SessionStore'; - -const ManageSubgroupsPage = () => { - const { - filteredSubgroups, - searchQuery, - subgroupName, - cacheTriggerMeters, - error, - setSearchQuery, - setSubgroupName, - setCacheTriggerMeters, - addSubgroup, - updateCacheForSubgroup, - } = useSubgroupAdminViewModel(); - const { currentIsBusiness } = getSession(); - - if (!currentIsBusiness) { - return ( - - - - - Subgroups are managed automatically for individual accounts. - - - - - ); - } - - return ( - - - - - + Create Subgroup - - - - {filteredSubgroups.length === 0 && ( - - {searchQuery ? 'No subgroups match your search.' : 'No subgroups yet. Create one to get started.'} - - )} - {filteredSubgroups.map((subgroup) => ( - { - updateCacheForSubgroup(subgroup.SGid, (subgroup.CacheTriggerMeters || 20) + 5); - }} - /> - ))} - - - Create New Subgroup - - - - Create - - - {!!error && {error}} - - - - ); -}; - -const styles = StyleSheet.create({ - wrapper: { - flex: 1, - backgroundColor: '#ffffff', - }, - container: { - flex: 1, - padding: 20, - flexDirection: 'column', - }, - message: { - textAlign: 'center', - color: '#6b7280', - fontSize: 15, - }, - createButton: { - backgroundColor: '#2563eb', - borderWidth: 1, - borderColor: '#2563eb', - borderRadius: 8, - paddingVertical: 10, - paddingHorizontal: 12, - alignItems: 'center', - marginBottom: 12, - }, - createButtonText: { - color: '#ffffff', - fontWeight: '600', - fontSize: 14, - }, - cardList: { - flex: 1, - gap: 8, - }, - emptyText: { - textAlign: 'center', - color: '#9ca3af', - fontSize: 14, - marginTop: 20, - }, - createForm: { - gap: 10, - paddingTop: 12, - borderTopWidth: 1, - borderTopColor: '#d1d5db', - marginTop: 12, - }, - formTitle: { - fontSize: 13, - fontWeight: '600', - color: '#111827', - }, - input: { - borderWidth: 1, - borderColor: '#d1d5db', - borderRadius: 8, - paddingHorizontal: 12, - paddingVertical: 10, - }, - submitButton: { - backgroundColor: '#4b5563', - borderRadius: 8, - alignItems: 'center', - paddingVertical: 10, - }, - submitButtonText: { - color: '#ffffff', - fontWeight: '600', - }, - errorText: { - color: '#dc2626', - marginTop: 8, - }, -}); - -export default ManageSubgroupsPage; - diff --git a/app/Screens/MapPage.js b/app/Screens/MapPage.js deleted file mode 100644 index cbc6e56..0000000 --- a/app/Screens/MapPage.js +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import { View, Text, StyleSheet } from 'react-native'; -import MemberFooterNavbar from '../components/MemberFooterNavbar'; -import useMapViewModel from '../ViewModel/useMapViewModel'; -import ScreenHeader from '../components/ScreenHeader'; - -const MapPage = () => { - const { points } = useMapViewModel(); - - return ( - - - - Map and hunt markers appear here. - Points loaded: {points.length} - - - - ); -}; - -const styles = StyleSheet.create({ - wrapper: { - flex: 1, - backgroundColor: '#ffffff', - }, - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - padding: 20, - gap: 6, - }, - body: { - color: '#4b5563', - }, -}); - -export default MapPage; - diff --git a/app/Screens/TeamPage.js b/app/Screens/TeamPage.js deleted file mode 100644 index 34e8035..0000000 --- a/app/Screens/TeamPage.js +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import { View, Text, StyleSheet } from 'react-native'; -import MemberFooterNavbar from '../components/MemberFooterNavbar'; -import useTeamViewModel from '../ViewModel/useTeamViewModel'; -import ScreenHeader from '../components/ScreenHeader'; - -const TeamPage = () => { - const { team } = useTeamViewModel(); - - return ( - - - - Team details and members appear here. - {team?.team?.TeamName || 'No team joined yet'} - - - - ); -}; - -const styles = StyleSheet.create({ - wrapper: { - flex: 1, - backgroundColor: '#ffffff', - }, - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - padding: 20, - gap: 6, - }, - body: { - color: '#4b5563', - }, -}); - -export default TeamPage; - diff --git a/app/ViewModel/useAdminViewModel.js b/app/ViewModel/useAdminViewModel.js deleted file mode 100644 index 0893f70..0000000 --- a/app/ViewModel/useAdminViewModel.js +++ /dev/null @@ -1,29 +0,0 @@ -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; - diff --git a/app/ViewModel/useAuthViewModel.js b/app/ViewModel/useAuthViewModel.js deleted file mode 100644 index d45b1c6..0000000 --- a/app/ViewModel/useAuthViewModel.js +++ /dev/null @@ -1,71 +0,0 @@ -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; - diff --git a/app/ViewModel/useGameTypeViewModel.js b/app/ViewModel/useGameTypeViewModel.js deleted file mode 100644 index dfe1e10..0000000 --- a/app/ViewModel/useGameTypeViewModel.js +++ /dev/null @@ -1,23 +0,0 @@ -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; - diff --git a/app/ViewModel/useLobbyViewModel.js b/app/ViewModel/useLobbyViewModel.js deleted file mode 100644 index 3a98a0d..0000000 --- a/app/ViewModel/useLobbyViewModel.js +++ /dev/null @@ -1,51 +0,0 @@ -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; - diff --git a/app/ViewModel/useManagePrivateSubtaskViewModel.js b/app/ViewModel/useManagePrivateSubtaskViewModel.js deleted file mode 100644 index 30ed155..0000000 --- a/app/ViewModel/useManagePrivateSubtaskViewModel.js +++ /dev/null @@ -1,97 +0,0 @@ -import {useState} from 'react'; -import {createPrivateGame, getCreatedPrivateGame, joinAsAdmin} from '../API/API'; -import {getSession, setSessionMembership} from '../Model/SessionStore'; - -const useManagePrivateSubtaskViewModel = () => { - const [businessOrSchoolName, setBusinessOrSchoolName] = useState(''); - const [error, setError] = useState(''); - - const isBusinessUser = Boolean(getSession().currentIsBusiness); - - const findExistingPrivateGame = async () => { - const { currentUid } = getSession(); - if (!currentUid) { - return null; - } - return getCreatedPrivateGame(currentUid); - }; - - 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: isBusinessUser, - }); - - if (isBusinessUser) { - setSessionMembership({ gid: group.Gid, sgid: 0, isAcceptedAdmin: true }); - } else { - setSessionMembership({ gid: group.Gid, sgid: 1, isAcceptedAdmin: false }); - } - - return group; - } catch (e) { - setError(e.message); - return null; - } - }; - - const resolveIndividualPrivateRoute = async () => { - setError(''); - try { - const existingGroup = await findExistingPrivateGame(); - if (existingGroup) { - setSessionMembership({ gid: existingGroup.Gid, sgid: 1, isAcceptedAdmin: false }); - return '(routes)/manage-game'; - } - - const created = await createNewPrivateGame(); - if (!created) { - return null; - } - return '(routes)/create-game'; - } 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) { - setSessionMembership({ gid, sgid: 0, isAcceptedAdmin: !response.queued }); - } - if (response?.queued) { - setError('Admin access request submitted. You will see admin tools after approval.'); - } - return response; - } catch (e) { - setError(e.message); - return null; - } - }; - - return { - businessOrSchoolName, - error, - isBusinessUser, - setBusinessOrSchoolName, - createNewPrivateGame, - resolveIndividualPrivateRoute, - joinAdminForPrivate, - }; -}; - -export default useManagePrivateSubtaskViewModel; - diff --git a/app/ViewModel/useMapViewModel.js b/app/ViewModel/useMapViewModel.js deleted file mode 100644 index e0eeeeb..0000000 --- a/app/ViewModel/useMapViewModel.js +++ /dev/null @@ -1,30 +0,0 @@ -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; - diff --git a/app/ViewModel/usePrivateGameViewModel.js b/app/ViewModel/usePrivateGameViewModel.js deleted file mode 100644 index 13f7721..0000000 --- a/app/ViewModel/usePrivateGameViewModel.js +++ /dev/null @@ -1,31 +0,0 @@ -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; - diff --git a/app/ViewModel/useSubgroupAdminViewModel.js b/app/ViewModel/useSubgroupAdminViewModel.js deleted file mode 100644 index 6937337..0000000 --- a/app/ViewModel/useSubgroupAdminViewModel.js +++ /dev/null @@ -1,82 +0,0 @@ -import {useCallback, useEffect, useMemo, useState} from 'react'; -import {createSubgroup, getVisibleSubgroups, 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 [searchQuery, setSearchQuery] = useState(''); - const { currentGid } = getSession(); - - const loadSubgroups = useCallback(async () => { - if (!currentGid) { - return; - } - const rows = await getVisibleSubgroups(currentGid); - setSubgroups(rows); - }, [currentGid]); - - const filteredSubgroups = useMemo(() => { - if (!searchQuery.trim()) { - return subgroups; - } - const q = searchQuery.trim().toLowerCase(); - return subgroups.filter((sg) => { - const nameMatch = sg.SubGroupName.toLowerCase().includes(q); - const idMatch = String(sg.SGid) === q; - return nameMatch || idMatch; - }); - }, [subgroups, searchQuery]); - - 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, - filteredSubgroups, - searchQuery, - subgroupName, - cacheTriggerMeters, - error, - setSearchQuery, - setSubgroupName, - setCacheTriggerMeters, - addSubgroup, - updateCacheForSubgroup, - }; -}; - -export default useSubgroupAdminViewModel; - diff --git a/app/ViewModel/useTeamViewModel.js b/app/ViewModel/useTeamViewModel.js deleted file mode 100644 index f918518..0000000 --- a/app/ViewModel/useTeamViewModel.js +++ /dev/null @@ -1,33 +0,0 @@ -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; - diff --git a/app/_layout.tsx b/app/_layout.tsx deleted file mode 100644 index a2edc37..0000000 --- a/app/_layout.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import {Stack} from "expo-router"; - -export default function RootLayout() { - return ( - - - - - - - - - - - - - - - - ); -} diff --git a/app/components/AdminFooterNavbar.js b/app/components/AdminFooterNavbar.js deleted file mode 100644 index afae78d..0000000 --- a/app/components/AdminFooterNavbar.js +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react'; -import { View, Pressable, Text, StyleSheet } from 'react-native'; -import { useNavigation } from '@react-navigation/native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; - -const AdminFooterNavbar = () => { - const navigation = useNavigation(); - const insets = useSafeAreaInsets(); - - return ( - - navigation.navigate('(routes)/admin-hub')}> - Admin Hub - - navigation.navigate('(routes)/game-lobby')}> - Manage Game - - navigation.navigate('(routes)/team')}> - Manage Team - - - ); -}; - -const styles = StyleSheet.create({ - container: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - borderTopWidth: 1, - borderColor: '#d4d4d8', - paddingHorizontal: 16, - paddingTop: 14, - backgroundColor: '#ffffff', - minHeight: 86, - }, - button: { - flex: 1, - alignItems: 'center', - paddingVertical: 8, - }, - text: { - fontSize: 14, - fontWeight: '600', - color: '#111827', - }, -}); - -export default AdminFooterNavbar; - diff --git a/app/components/MemberFooterNavbar.js b/app/components/MemberFooterNavbar.js deleted file mode 100644 index 4af4578..0000000 --- a/app/components/MemberFooterNavbar.js +++ /dev/null @@ -1,54 +0,0 @@ -import React from 'react'; -import { View, Pressable, Text, StyleSheet } from 'react-native'; -import { useNavigation } from '@react-navigation/native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; - -const MemberFooterNavbar = ({ visible = false }) => { - const navigation = useNavigation(); - const insets = useSafeAreaInsets(); - - if (!visible) { - return null; - } - - return ( - - navigation.navigate('(routes)/game-lobby')}> - Lobby - - navigation.navigate('(routes)/team')}> - Team - - navigation.navigate('(routes)/map')}> - Map - - - ); -}; - -const styles = StyleSheet.create({ - container: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - borderTopWidth: 1, - borderColor: '#d4d4d8', - paddingHorizontal: 16, - paddingTop: 14, - backgroundColor: '#ffffff', - minHeight: 86, - }, - button: { - flex: 1, - alignItems: 'center', - paddingVertical: 8, - }, - text: { - fontSize: 14, - fontWeight: '600', - color: '#111827', - }, -}); - -export default MemberFooterNavbar; - diff --git a/app/components/ScreenHeader.js b/app/components/ScreenHeader.js deleted file mode 100644 index 52e94d4..0000000 --- a/app/components/ScreenHeader.js +++ /dev/null @@ -1,108 +0,0 @@ -import React from 'react'; -import {Pressable, StyleSheet, Text, View} from 'react-native'; -import {useSafeAreaInsets} from 'react-native-safe-area-context'; -import {useRouter, useSegments} from 'expo-router'; -import {getSession, setSessionGroup, setSessionTeam, setSessionUser} from '../Model/SessionStore'; - -const ScreenHeader = ({ title, showBack = true }) => { - const insets = useSafeAreaInsets(); - const router = useRouter(); - const segments = useSegments(); - const session = getSession(); - - // Check if user is logged in by checking if currentUid exists - const isLoggedIn = Boolean(session.currentUid); - - // Pages where back navigation should be disabled - const blockedLastSegments = ['login', 'register', 'index']; - - // compute last meaningful segment (filter out route groups like "(routes)") - const meaningfulSegments = segments.filter((s) => !s.startsWith('(')); - const lastSegment = meaningfulSegments.length > 0 ? meaningfulSegments[meaningfulSegments.length - 1] : 'index'; - const isBlocked = blockedLastSegments.includes(lastSegment); - - - const handleBack = () => { - if (isBlocked || !showBack) return; - try { - router.back(); - } catch { - // fallback: nothing - } - }; - - const handleLogout = () => { - // clear session and route to login (replace so user cannot go back) - try { - setSessionUser(null); - setSessionGroup(null); - setSessionTeam(null); - } catch { - // ignore - } - try { - router.replace('/login'); - } catch { - // noop - } - }; - - return ( - - {showBack && !isBlocked ? ( - - ← - - ) : ( - - )} - {title} - {isLoggedIn ? ( - - ⎋ - - ) : ( - - )} - - ); -}; - -const styles = StyleSheet.create({ - container: { - backgroundColor: '#000000', - paddingHorizontal: 16, - paddingBottom: 12, - minHeight: 72, - flexDirection: 'row', - alignItems: 'flex-end', - justifyContent: 'space-between', - }, - title: { - color: '#ffffff', - fontSize: 20, - fontWeight: '700', - }, - backButton: { - width: 44, - height: 44, - justifyContent: 'center', - alignItems: 'center', - }, - backText: { - color: '#ffffff', - fontSize: 22, - lineHeight: 22, - }, - backButtonPlaceholder: { - width: 44, - height: 44, - }, - rightPlaceholder: { - width: 44, - height: 44, - }, -}); - -export default ScreenHeader; - diff --git a/app/components/UI/EntityCard.jsx b/app/components/UI/EntityCard.jsx deleted file mode 100644 index 9458b2a..0000000 --- a/app/components/UI/EntityCard.jsx +++ /dev/null @@ -1,64 +0,0 @@ -import React from 'react'; -import {Pressable, StyleSheet, Text, View} from 'react-native'; - -const EntityCard = ({ - title, - subtitle, - metaLabel1, - metaValue1, - metaLabel2, - metaValue2, - metaLabel3, - metaValue3, - onPress, -}) => { - return ( - - {title} - {subtitle && {subtitle}} - - {metaLabel1 && metaValue1 && ( - {metaLabel1}: {metaValue1} - )} - {metaLabel2 && metaValue2 && ( - {metaLabel2}: {metaValue2} - )} - {metaLabel3 && metaValue3 && ( - {metaLabel3}: {metaValue3} - )} - - - ); -}; - -const styles = StyleSheet.create({ - card: { - borderWidth: 1, - borderColor: '#d1d5db', - borderRadius: 8, - padding: 12, - marginBottom: 10, - backgroundColor: '#ffffff', - }, - title: { - fontSize: 15, - fontWeight: '700', - color: '#111827', - marginBottom: 4, - }, - subtitle: { - fontSize: 13, - color: '#6b7280', - marginBottom: 8, - }, - metaContainer: { - gap: 4, - }, - metaText: { - fontSize: 13, - color: '#4b5563', - }, -}); - -export default EntityCard; - diff --git a/app/components/UI/SearchBar.jsx b/app/components/UI/SearchBar.jsx deleted file mode 100644 index b7a111c..0000000 --- a/app/components/UI/SearchBar.jsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import {StyleSheet, TextInput, View} from 'react-native'; - -const SearchBar = ({ placeholder = 'Search...', value, onChangeText, style = {} }) => { - return ( - - - - ); -}; - -const styles = StyleSheet.create({ - container: { - marginBottom: 12, - }, - input: { - borderWidth: 1, - borderColor: '#d1d5db', - borderRadius: 8, - paddingHorizontal: 12, - paddingVertical: 10, - fontSize: 14, - color: '#111827', - }, -}); - -export default SearchBar; - diff --git a/eslint.config.js b/eslint.config.js index 5025da6..19c6e42 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,4 +1,3 @@ -// https://docs.expo.dev/guides/using-eslint/ const { defineConfig } = require('eslint/config'); const expoConfig = require('eslint-config-expo/flat'); diff --git a/package-lock.json b/package-lock.json index 4e2e1f2..94e37af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,26 +8,26 @@ "name": "mad_treasure_hunt_app", "version": "1.0.0", "dependencies": { - "@expo/vector-icons": "^15.0.3", - "@react-navigation/bottom-tabs": "^7.4.0", - "@react-navigation/elements": "^2.6.3", - "@react-navigation/native": "^7.1.8", + "@react-navigation/native": "^7.1.18", + "@react-navigation/native-stack": "^7.3.28", "expo": "~54.0.33", "expo-constants": "~18.0.13", "expo-font": "~14.0.11", "expo-haptics": "~15.0.8", "expo-image": "~3.0.11", - "expo-linking": "~8.0.11", - "expo-router": "~6.0.23", + "expo-location": "~19.0.7", + "expo-sensors": "~15.0.7", "expo-splash-screen": "~31.0.13", "expo-status-bar": "~3.0.9", "expo-symbols": "~1.0.8", "expo-system-ui": "~6.0.9", "expo-web-browser": "~15.0.10", + "geolib": "^3.3.4", "react": "19.1.0", "react-dom": "19.1.0", "react-native": "0.81.5", "react-native-gesture-handler": "~2.28.0", + "react-native-maps": "1.20.1", "react-native-reanimated": "~4.1.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", @@ -35,10 +35,8 @@ "react-native-worklets": "0.5.1" }, "devDependencies": { - "@types/react": "~19.1.0", "eslint": "^9.25.0", - "eslint-config-expo": "~10.0.0", - "typescript": "~5.9.2" + "eslint-config-expo": "~10.0.0" } }, "node_modules/@0no-co/graphql.web": { @@ -1906,28 +1904,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@expo/metro-runtime": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-6.1.2.tgz", - "integrity": "sha512-nvM+Qv45QH7pmYvP8JB1G8JpScrWND3KrMA6ZKe62cwwNiX/BjHU28Ear0v/4bQWXlOY0mv6B8CDIm8JxXde9g==", - "dependencies": { - "anser": "^1.4.9", - "pretty-format": "^29.7.0", - "stacktrace-parser": "^0.1.10", - "whatwg-fetch": "^3.0.0" - }, - "peerDependencies": { - "expo": "*", - "react": "*", - "react-dom": "*", - "react-native": "*" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, "node_modules/@expo/osascript": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.4.2.tgz", @@ -2376,181 +2352,6 @@ "node": ">=12.4.0" } }, - "node_modules/@radix-ui/primitive": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", - "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==" - }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", - "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", - "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", - "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", - "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@react-native/assets-registry": { "version": "0.81.5", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.5.tgz", @@ -2767,27 +2568,11 @@ "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.81.5.tgz", "integrity": "sha512-0HuJ8YtqlTVRXGZuGeBejLE04wSQsibpTI+RGOyVqxZvgtlLLC/Ssw0UmbHhT4lYMp2fhdtvKZSs5emWB1zR/g==" }, - "node_modules/@react-navigation/bottom-tabs": { - "version": "7.15.5", - "resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.15.5.tgz", - "integrity": "sha512-wQHredlCrRmShWQ1vF4HUcLdaiJ8fUgnbaeQH7BJ7MQVQh4mdzab0IOY/4QSmUyNRB350oyu1biTycyQ5FKWMQ==", - "dependencies": { - "@react-navigation/elements": "^2.9.10", - "color": "^4.2.3", - "sf-symbols-typescript": "^2.1.0" - }, - "peerDependencies": { - "@react-navigation/native": "^7.1.33", - "react": ">= 18.2.0", - "react-native": "*", - "react-native-safe-area-context": ">= 4.0.0", - "react-native-screens": ">= 4.0.0" - } - }, "node_modules/@react-navigation/core": { - "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.16.1.tgz", - "integrity": "sha512-xhquoyhKdqDfiL7LuupbwYnmauUGfVFGDEJO34m26k8zSN1eDjQ2stBZcHN8ILOI1PrG9885nf8ZmfaQxPS0ww==", + "version": "7.17.2", + "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.17.2.tgz", + "integrity": "sha512-Rt2OZwcgOmjv401uLGAKaRM6xo0fiBce/A7LfRHI1oe5FV+KooWcgAoZ2XOtgKj6UzVMuQWt3b2e6rxo/mDJRA==", + "license": "MIT", "dependencies": { "@react-navigation/routers": "^7.5.3", "escape-string-regexp": "^4.0.0", @@ -2803,9 +2588,10 @@ } }, "node_modules/@react-navigation/elements": { - "version": "2.9.10", - "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.9.10.tgz", - "integrity": "sha512-N8tuBekzTRb0pkMHFJGvmC6Q5OisSbt6gzvw7RHMnp4NDo5auVllT12sWFaTXf8mTduaLKNSrD/NZNaOqThCBg==", + "version": "2.9.14", + "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.9.14.tgz", + "integrity": "sha512-lKqzu+su2pI/YIZmR7L7xdOs4UL+rVXKJAMpRMBrwInEy96SjIFst6QDGpE89Dunnu3VjVpjWfByo9f2GWBHDQ==", + "license": "MIT", "dependencies": { "color": "^4.2.3", "use-latest-callback": "^0.2.4", @@ -2813,7 +2599,7 @@ }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", - "@react-navigation/native": "^7.1.33", + "@react-navigation/native": "^7.2.2", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" @@ -2825,11 +2611,12 @@ } }, "node_modules/@react-navigation/native": { - "version": "7.1.33", - "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.33.tgz", - "integrity": "sha512-DpFdWGcgLajKZ1TuIvDNQsblN2QaUFWpTQaB8v7WRP9Mix8H/6TFoIrZd93pbymI2hybd6UYrD+lI408eWVcfw==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.2.2.tgz", + "integrity": "sha512-kem1Ko2BcbAjmbQIv66dNmr6EtfDut3QU0qjsVhMnLLhktwyXb6FzZYp8gTrUb6AvkAbaJoi+BF5Pl55pAUa5w==", + "license": "MIT", "dependencies": { - "@react-navigation/core": "^7.16.1", + "@react-navigation/core": "^7.17.2", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", @@ -2841,17 +2628,18 @@ } }, "node_modules/@react-navigation/native-stack": { - "version": "7.14.4", - "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.14.4.tgz", - "integrity": "sha512-HFEnM5Q7JY3FmmiolD/zvgY+9sxZAyVGPZJoz7BdTvJmi1VHOdplf24YiH45mqeitlGnaOlvNT55rH4abHJ5eA==", + "version": "7.14.10", + "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.14.10.tgz", + "integrity": "sha512-mCbYbYhi7Em2R2nEgwYGdLU38smy+KK+HMMVcwuzllWsF3Qb+jOUEYbB6Or7LvE7SS77BZ6sHdx4HptCEv50hQ==", + "license": "MIT", "dependencies": { - "@react-navigation/elements": "^2.9.10", + "@react-navigation/elements": "^2.9.14", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0", "warn-once": "^0.1.1" }, "peerDependencies": { - "@react-navigation/native": "^7.1.33", + "@react-navigation/native": "^7.2.2", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", @@ -2862,6 +2650,7 @@ "version": "7.5.3", "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.5.3.tgz", "integrity": "sha512-1tJHg4KKRJuQ1/EvJxatrMef3NZXEPzwUIUZ3n1yJ2t7Q97siwRtbynRpQG9/69ebbtiZ8W3ScOZF/OmhvM4Rg==", + "license": "MIT", "dependencies": { "nanoid": "^3.3.11" } @@ -2946,6 +2735,12 @@ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -3000,15 +2795,6 @@ "undici-types": "~7.18.0" } }, - "node_modules/@types/react": { - "version": "19.1.17", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.17.tgz", - "integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==", - "devOptional": true, - "dependencies": { - "csstype": "^3.0.2" - } - }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -3736,17 +3522,6 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, - "node_modules/aria-hidden": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", - "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", @@ -4480,11 +4255,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" - }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -4510,6 +4280,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" @@ -4538,6 +4309,7 @@ "version": "1.9.1", "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" @@ -4678,12 +4450,6 @@ "hyphenate-style-name": "^1.0.3" } }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true - }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -4755,6 +4521,7 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "license": "MIT", "engines": { "node": ">=0.10" } @@ -4859,11 +4626,6 @@ "node": ">=8" } }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" - }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -5692,17 +5454,13 @@ "react": "*" } }, - "node_modules/expo-linking": { - "version": "8.0.11", - "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.11.tgz", - "integrity": "sha512-+VSaNL5om3kOp/SSKO5qe6cFgfSIWnnQDSbA7XLs3ECkYzXRquk5unxNS3pg7eK5kNUmQ4kgLI7MhTggAEUBLA==", - "dependencies": { - "expo-constants": "~18.0.12", - "invariant": "^2.2.4" - }, + "node_modules/expo-location": { + "version": "19.0.8", + "resolved": "https://registry.npmjs.org/expo-location/-/expo-location-19.0.8.tgz", + "integrity": "sha512-H/FI75VuJ1coodJbbMu82pf+Zjess8X8Xkiv9Bv58ZgPKS/2ztjC1YO1/XMcGz7+s9DrbLuMIw22dFuP4HqneA==", + "license": "MIT", "peerDependencies": { - "react": "*", - "react-native": "*" + "expo": "*" } }, "node_modules/expo-modules-autolinking": { @@ -5732,248 +5490,17 @@ "react-native": "*" } }, - "node_modules/expo-router": { - "version": "6.0.23", - "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-6.0.23.tgz", - "integrity": "sha512-qCxVAiCrCyu0npky6azEZ6dJDMt77OmCzEbpF6RbUTlfkaCA417LvY14SBkk0xyGruSxy/7pvJOI6tuThaUVCA==", + "node_modules/expo-sensors": { + "version": "15.0.8", + "resolved": "https://registry.npmjs.org/expo-sensors/-/expo-sensors-15.0.8.tgz", + "integrity": "sha512-ttibOSCYjFAMIfjV+vVukO1v7GKlbcPRfxcRqbTaSMGneewDwVSXbGFImY530fj1BR3mWq4n9jHnuDp8tAEY9g==", + "license": "MIT", "dependencies": { - "@expo/metro-runtime": "^6.1.2", - "@expo/schema-utils": "^0.1.8", - "@radix-ui/react-slot": "1.2.0", - "@radix-ui/react-tabs": "^1.1.12", - "@react-navigation/bottom-tabs": "^7.4.0", - "@react-navigation/native": "^7.1.8", - "@react-navigation/native-stack": "^7.3.16", - "client-only": "^0.0.1", - "debug": "^4.3.4", - "escape-string-regexp": "^4.0.0", - "expo-server": "^1.0.5", - "fast-deep-equal": "^3.1.3", - "invariant": "^2.2.4", - "nanoid": "^3.3.8", - "query-string": "^7.1.3", - "react-fast-compare": "^3.2.2", - "react-native-is-edge-to-edge": "^1.1.6", - "semver": "~7.6.3", - "server-only": "^0.0.1", - "sf-symbols-typescript": "^2.1.0", - "shallowequal": "^1.1.0", - "use-latest-callback": "^0.2.1", - "vaul": "^1.1.2" + "invariant": "^2.2.4" }, "peerDependencies": { - "@expo/metro-runtime": "^6.1.2", - "@react-navigation/drawer": "^7.5.0", - "@testing-library/react-native": ">= 12.0.0", "expo": "*", - "expo-constants": "^18.0.13", - "expo-linking": "^8.0.11", - "react": "*", - "react-dom": "*", - "react-native": "*", - "react-native-gesture-handler": "*", - "react-native-reanimated": "*", - "react-native-safe-area-context": ">= 5.4.0", - "react-native-screens": "*", - "react-native-web": "*", - "react-server-dom-webpack": "~19.0.4 || ~19.1.5 || ~19.2.4" - }, - "peerDependenciesMeta": { - "@react-navigation/drawer": { - "optional": true - }, - "@testing-library/react-native": { - "optional": true - }, - "react-dom": { - "optional": true - }, - "react-native-gesture-handler": { - "optional": true - }, - "react-native-reanimated": { - "optional": true - }, - "react-native-web": { - "optional": true - }, - "react-server-dom-webpack": { - "optional": true - } - } - }, - "node_modules/expo-router/node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/expo-router/node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/expo-router/node_modules/@radix-ui/react-presence": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/expo-router/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/expo-router/node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/expo-router/node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", - "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/expo-router/node_modules/@radix-ui/react-tabs": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", - "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/expo-router/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" + "react-native": "*" } }, "node_modules/expo-server": { @@ -6294,6 +5821,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -6476,6 +6004,12 @@ "node": ">=6.9.0" } }, + "node_modules/geolib": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/geolib/-/geolib-3.3.4.tgz", + "integrity": "sha512-EicrlLLL3S42gE9/wde+11uiaYAaeSVDwCUIv2uMIoRBfNJCn8EsSI+6nS3r4TCKDO6+RQNM9ayLq2at+oZQWQ==", + "license": "MIT" + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -6508,14 +6042,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "engines": { - "node": ">=6" - } - }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -6995,7 +6521,8 @@ "node_modules/is-arrayish": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", - "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==" + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" }, "node_modules/is-async-function": { "version": "2.1.1", @@ -9338,6 +8865,7 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", + "license": "MIT", "dependencies": { "decode-uri-component": "^0.2.2", "filter-obj": "^1.1.0", @@ -9417,11 +8945,6 @@ "react": "^19.1.0" } }, - "node_modules/react-fast-compare": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", - "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" - }, "node_modules/react-freeze": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.4.tgz", @@ -9436,7 +8959,8 @@ "node_modules/react-is": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", - "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==" + "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", + "license": "MIT" }, "node_modules/react-native": { "version": "0.81.5", @@ -9517,6 +9041,28 @@ "react-native": "*" } }, + "node_modules/react-native-maps": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/react-native-maps/-/react-native-maps-1.20.1.tgz", + "integrity": "sha512-NZI3B5Z6kxAb8gzb2Wxzu/+P2SlFIg1waHGIpQmazDSCRkNoHNY4g96g+xS0QPSaG/9xRBbDNnd2f2/OW6t6LQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "^7946.0.13" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": ">= 17.0.1", + "react-native": ">= 0.64.3", + "react-native-web": ">= 0.11" + }, + "peerDependenciesMeta": { + "react-native-web": { + "optional": true + } + } + }, "node_modules/react-native-reanimated": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.6.tgz", @@ -9706,72 +9252,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-remove-scroll": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", - "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", - "dependencies": { - "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.3", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.3" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", - "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", - "dependencies": { - "react-style-singleton": "^2.2.2", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-style-singleton": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", - "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", - "dependencies": { - "get-nonce": "^1.0.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -10185,11 +9665,6 @@ "node": ">= 0.8" } }, - "node_modules/server-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", - "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==" - }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -10254,11 +9729,6 @@ "node": ">=10" } }, - "node_modules/shallowequal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -10380,6 +9850,7 @@ "version": "0.2.4", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", "dependencies": { "is-arrayish": "^0.3.1" } @@ -10442,6 +9913,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "license": "MIT", "engines": { "node": ">=6" } @@ -10525,6 +9997,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", + "license": "MIT", "engines": { "node": ">=4" } @@ -10972,7 +10445,9 @@ "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "optional": true }, "node_modules/type-check": { "version": "0.4.0", @@ -11081,6 +10556,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11262,59 +10738,20 @@ "punycode": "^2.1.0" } }, - "node_modules/use-callback-ref": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", - "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/use-latest-callback": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.2.6.tgz", "integrity": "sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg==", + "license": "MIT", "peerDependencies": { "react": ">=16.8" } }, - "node_modules/use-sidecar": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", - "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/use-sync-external-store": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } @@ -11351,188 +10788,6 @@ "node": ">= 0.8" } }, - "node_modules/vaul": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", - "integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==", - "dependencies": { - "@radix-ui/react-dialog": "^1.1.1" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" - } - }, - "node_modules/vaul/node_modules/@radix-ui/react-dialog": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", - "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/vaul/node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", - "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/vaul/node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/vaul/node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/vaul/node_modules/@radix-ui/react-presence": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/vaul/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/vaul/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/vlq": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz", diff --git a/package.json b/package.json index eabd380..ca70bae 100644 --- a/package.json +++ b/package.json @@ -1,45 +1,45 @@ { "name": "mad_treasure_hunt_app", - "main": "expo-router/entry", + "main": "node_modules/expo/AppEntry.js", "version": "1.0.0", "scripts": { + "api": "node server/server.js", + "api:standalone": "npm --prefix server start", "start": "expo start", - "reset-project": "node ./scripts/reset-project.js", + "start:api": "npm run api", "android": "expo run:android", "ios": "expo run:ios", "web": "expo start --web", "lint": "expo lint" }, "dependencies": { - "@expo/vector-icons": "^15.0.3", - "@react-navigation/bottom-tabs": "^7.4.0", - "@react-navigation/elements": "^2.6.3", - "@react-navigation/native": "^7.1.8", + "@react-navigation/native": "^7.1.18", + "@react-navigation/native-stack": "^7.3.28", "expo": "~54.0.33", "expo-constants": "~18.0.13", "expo-font": "~14.0.11", "expo-haptics": "~15.0.8", "expo-image": "~3.0.11", - "expo-linking": "~8.0.11", - "expo-router": "~6.0.23", "expo-splash-screen": "~31.0.13", "expo-status-bar": "~3.0.9", "expo-symbols": "~1.0.8", "expo-system-ui": "~6.0.9", + "expo-location": "~19.0.7", "expo-web-browser": "~15.0.10", + "geolib": "^3.3.4", "react": "19.1.0", "react-dom": "19.1.0", "react-native": "0.81.5", "react-native-gesture-handler": "~2.28.0", + "react-native-maps": "1.20.1", "react-native-worklets": "0.5.1", + "expo-sensors": "~15.0.7", "react-native-reanimated": "~4.1.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", "react-native-web": "~0.21.0" }, "devDependencies": { - "@types/react": "~19.1.0", - "typescript": "~5.9.2", "eslint": "^9.25.0", "eslint-config-expo": "~10.0.0" }, diff --git a/src/components/UI/Button.js b/src/components/UI/Button.js new file mode 100644 index 0000000..7c3bf4e --- /dev/null +++ b/src/components/UI/Button.js @@ -0,0 +1,38 @@ +import {Pressable, StyleSheet, Text, View} from "react-native"; + +export const Button = ({ label, icon, onClick, styleLabel, styleButton }) => { + return ( + + {icon ? icon : null} + {label} + + ); +}; + +export const ButtonTray = ({ children }) => { + return {children}; +}; + +const styles = StyleSheet.create({ + button: { + minHeight: 50, + borderWidth: 1, + borderRadius: 7, + borderColor: "grey", + backgroundColor: "white", + alignItems: "center", + justifyContent: "center", + padding: 3, + flex: 1, + flexDirection: "row", + gap: 5, + }, + label: { + fontSize: 16, + }, + buttonTray: { + flexDirection: "row", + gap: 15, + }, +}); + diff --git a/src/components/gameplay/AdminCacheEditorView.js b/src/components/gameplay/AdminCacheEditorView.js new file mode 100644 index 0000000..9635b6b --- /dev/null +++ b/src/components/gameplay/AdminCacheEditorView.js @@ -0,0 +1,46 @@ +import React from 'react'; +import MapView, {Circle, Marker} from 'react-native-maps'; + +const AdminCacheEditorView = ({ userLocation, caches, onAddCache, onMoveCache }) => { + if (!userLocation) { + return null; + } + + return ( + onAddCache(event.nativeEvent.coordinate)} + showsUserLocation + > + {(caches || []).map((cache) => ( + + onMoveCache(cache.id, event.nativeEvent.coordinate)} + title={`Cache ${cache.id}`} + /> + + + ))} + + ); +}; + +export default AdminCacheEditorView; + diff --git a/src/components/gameplay/ClaimTimerView.js b/src/components/gameplay/ClaimTimerView.js new file mode 100644 index 0000000..6c723bc --- /dev/null +++ b/src/components/gameplay/ClaimTimerView.js @@ -0,0 +1,48 @@ +import React, {useEffect, useState} from 'react'; +import {StyleSheet, Text, View} from 'react-native'; + +const ClaimTimerView = ({ cache, onClaimSuccess, isClaiming }) => { + const [timeLeft, setTimeLeft] = useState(5); + + useEffect(() => { + let timer; + if (isClaiming && timeLeft > 0) { + timer = setTimeout(() => setTimeLeft((prev) => prev - 1), 1000); + } else if (isClaiming && timeLeft === 0 && cache) { + onClaimSuccess(cache.id); + } else { + setTimeLeft(5); + } + return () => clearTimeout(timer); + }, [isClaiming, timeLeft, cache, onClaimSuccess]); + + if (!isClaiming) { + return null; + } + + return ( + + Hold steady. Claiming in {timeLeft}... + + ); +}; + +const styles = StyleSheet.create({ + overlay: { + position: 'absolute', + bottom: 24, + alignSelf: 'center', + backgroundColor: 'rgba(0,0,0,0.75)', + paddingHorizontal: 16, + paddingVertical: 12, + borderRadius: 10, + }, + text: { + color: '#ffffff', + fontSize: 16, + fontWeight: '700', + }, +}); + +export default ClaimTimerView; + diff --git a/src/components/gameplay/PlayerMapView.js b/src/components/gameplay/PlayerMapView.js new file mode 100644 index 0000000..707901c --- /dev/null +++ b/src/components/gameplay/PlayerMapView.js @@ -0,0 +1,46 @@ +import React from 'react'; +import MapView, {Circle} from 'react-native-maps'; +import ClaimTimerView from './ClaimTimerView'; + +const PlayerMapView = ({ userLocation, visibleCache, isClaiming, onClaimSuccess }) => { + if (!userLocation) { + return null; + } + + return ( + <> + + {visibleCache ? ( + + ) : null} + + + + + ); +}; + +export default PlayerMapView; + diff --git a/src/components/layout/BottomNavbar.js b/src/components/layout/BottomNavbar.js new file mode 100644 index 0000000..8ad987d --- /dev/null +++ b/src/components/layout/BottomNavbar.js @@ -0,0 +1,74 @@ +import {Pressable, StyleSheet, Text, View} from 'react-native'; + +const BottomNavbar = ({ navigation, routeName }) => { + const tabs = [ + { + label: 'Map', + route: 'MapScreen', + onPress: () => navigation.navigate('Game', { screen: 'MapScreen' }), + }, + { + label: 'Leaderboard', + route: 'LeaderboardScreen', + onPress: () => navigation.navigate('Game', { screen: 'LeaderboardScreen' }), + }, + { + label: 'Current Team', + route: 'TeamScreen', + onPress: () => navigation.navigate('Game', { screen: 'TeamScreen' }), + }, + ]; + + return ( + + {tabs.map((tab) => { + const isActive = routeName === tab.route; + return ( + + {tab.label} + + ); + })} + + ); +}; + +const styles = StyleSheet.create({ + container: { + minHeight: 64, + borderTopWidth: 1, + borderTopColor: '#e5e7eb', + backgroundColor: '#ffffff', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-around', + paddingHorizontal: 8, + }, + tab: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 12, + borderRadius: 8, + marginHorizontal: 4, + }, + tabActive: { + backgroundColor: '#eff6ff', + }, + tabText: { + color: '#374151', + fontSize: 12, + fontWeight: '600', + textAlign: 'center', + }, + tabTextActive: { + color: '#2563eb', + }, +}); + +export default BottomNavbar; + diff --git a/src/components/layout/Screen.js b/src/components/layout/Screen.js new file mode 100644 index 0000000..3c8a5f7 --- /dev/null +++ b/src/components/layout/Screen.js @@ -0,0 +1,53 @@ +import {StatusBar} from "expo-status-bar"; +import {StyleSheet, View} from "react-native"; +import {useNavigation, useRoute} from "@react-navigation/native"; +import TopNavbar from "./TopNavbar"; +import BottomNavbar from "./BottomNavbar"; + +const Screen = ({ children, style }) => { + const navigation = useNavigation(); + const route = useRoute(); + + const titleMap = { + LoginScreen: "Login", + RegisterScreen: "Register", + GameTypeScreen: "Game Type", + JoinPrivateGameScreen: "Join Private Game", + ManagePrivateGameScreen: "Manage Private Game", + CreateGameScreen: "Create Game", + ManageGameScreen: "Manage Game", + GameLobbyScreen: "Leaderboard", + LeaderboardScreen: "Leaderboard", + TeamScreen: "Current Team", + MapScreen: "Map", + AdminHubScreen: "Admin Hub", + ManageSubgroupsScreen: "Manage Subgroups", + }; + + const routeName = route.name; + const showBack = routeName !== "LoginScreen" && navigation.canGoBack(); + const pageTitle = titleMap[routeName] || "Treasure Hunt"; + + return ( + + + {children} + + + + ); +}; + +const styles = StyleSheet.create({ + screen: { + flex: 1, + backgroundColor: "#fff", + }, + content: { + flex: 1, + padding: 15, + }, +}); + +export default Screen; + diff --git a/src/components/layout/TopNavbar.js b/src/components/layout/TopNavbar.js new file mode 100644 index 0000000..088517a --- /dev/null +++ b/src/components/layout/TopNavbar.js @@ -0,0 +1,90 @@ +import {Pressable, StyleSheet, Text, View} from 'react-native'; +import {clearSession} from '../../hooks/SessionStore'; + +const TopNavbar = ({ title, showBack, navigation }) => { + const routeName = navigation.getState()?.routes?.[navigation.getState().index]?.name; + const showLogout = routeName !== 'LoginScreen' && routeName !== 'RegisterScreen'; + + const handleBack = () => { + if (navigation.canGoBack()) { + navigation.goBack(); + } + }; + + const handleLogout = () => { + clearSession(); + navigation.reset({ + index: 0, + routes: [{ name: 'Auth', params: { screen: 'LoginScreen' } }], + }); + }; + + return ( + + + {showBack ? ( + + {'<'} + + ) : null} + + {title} + + {showLogout ? ( + + Logout + + ) : null} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + minHeight: 56, + borderBottomWidth: 1, + borderBottomColor: '#e5e7eb', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 12, + backgroundColor: '#ffffff', + }, + leftSlot: { + width: 72, + alignItems: 'flex-start', + }, + rightSlot: { + width: 72, + alignItems: 'flex-end', + }, + iconButton: { + paddingVertical: 8, + paddingHorizontal: 10, + }, + iconText: { + fontSize: 20, + color: '#111827', + fontWeight: '700', + }, + title: { + flex: 1, + textAlign: 'center', + fontSize: 18, + fontWeight: '700', + color: '#111827', + }, + logoutButton: { + paddingVertical: 6, + paddingHorizontal: 8, + }, + logoutText: { + color: '#2563eb', + fontSize: 13, + fontWeight: '600', + }, +}); + +export default TopNavbar; + diff --git a/src/components/screens/AdminHubScreen.js b/src/components/screens/AdminHubScreen.js new file mode 100644 index 0000000..1e68d6f --- /dev/null +++ b/src/components/screens/AdminHubScreen.js @@ -0,0 +1,18 @@ +import React from 'react'; +import {StyleSheet, Text} from 'react-native'; +import Screen from '../layout/Screen'; + +const AdminHubScreen = () => ( + + Admin Hub + Admin hub setup goes here. + +); + +const styles = StyleSheet.create({ + container: { justifyContent: 'center', alignItems: 'center' }, + title: { fontSize: 24, fontWeight: 'bold' }, + body: { color: '#4b5563', fontSize: 15, marginTop: 10 }, +}); + +export default AdminHubScreen; diff --git a/src/components/screens/CreateGameScreen.js b/src/components/screens/CreateGameScreen.js new file mode 100644 index 0000000..d00cdae --- /dev/null +++ b/src/components/screens/CreateGameScreen.js @@ -0,0 +1,18 @@ +import React from 'react'; +import {StyleSheet, Text} from 'react-native'; +import Screen from '../layout/Screen'; + +const CreateGameScreen = () => ( + + Create Game + Create game setup goes here. + +); + +const styles = StyleSheet.create({ + container: { justifyContent: 'center', alignItems: 'center' }, + title: { fontSize: 24, fontWeight: 'bold' }, + body: { color: '#4b5563', fontSize: 15, marginTop: 10 }, +}); + +export default CreateGameScreen; diff --git a/src/components/screens/GameLobbyScreen.js b/src/components/screens/GameLobbyScreen.js new file mode 100644 index 0000000..6312cc6 --- /dev/null +++ b/src/components/screens/GameLobbyScreen.js @@ -0,0 +1,18 @@ +import React from 'react'; +import {StyleSheet, Text} from 'react-native'; +import Screen from '../layout/Screen'; + +const GameLobbyScreen = () => ( + + Game Lobby + Game lobby setup goes here. + +); + +const styles = StyleSheet.create({ + container: { justifyContent: 'center', alignItems: 'center' }, + title: { fontSize: 24, fontWeight: 'bold' }, + body: { color: '#4b5563', fontSize: 15, marginTop: 10 }, +}); + +export default GameLobbyScreen; diff --git a/src/components/screens/GameTypeScreen.js b/src/components/screens/GameTypeScreen.js new file mode 100644 index 0000000..e0c7dc4 --- /dev/null +++ b/src/components/screens/GameTypeScreen.js @@ -0,0 +1,50 @@ +import React from 'react'; +import {Pressable, StyleSheet, Text, View} from 'react-native'; +import useGameHook from '../../hooks/useGameHook'; +import Screen from '../layout/Screen'; + +const GameTypeScreen = ({ navigation }) => { + const { getGameTypes } = useGameHook(); + const gameTypes = getGameTypes(); + + return ( + + Select Game Type + + {gameTypes && gameTypes.map((type) => ( + navigation.navigate('GameLobbyScreen')}> + {type} + + ))} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + justifyContent: 'center', + }, + content: { + gap: 12, + flex: 1, + }, + title: { + fontSize: 24, + fontWeight: 'bold', + textAlign: 'center', + marginBottom: 20, + }, + button: { + backgroundColor: '#2563eb', + borderRadius: 8, + paddingVertical: 12, + alignItems: 'center', + }, + buttonText: { + color: '#ffffff', + fontWeight: '600', + }, +}); + +export default GameTypeScreen; diff --git a/src/components/screens/JoinPrivateGameScreen.js b/src/components/screens/JoinPrivateGameScreen.js new file mode 100644 index 0000000..4579833 --- /dev/null +++ b/src/components/screens/JoinPrivateGameScreen.js @@ -0,0 +1,30 @@ +import React from 'react'; +import {StyleSheet, Text} from 'react-native'; +import Screen from '../layout/Screen'; + +const JoinPrivateGameScreen = () => { + return ( + + Join Private Game + Join private game setup goes here. + + ); +}; + +const styles = StyleSheet.create({ + container: { + justifyContent: 'center', + alignItems: 'center', + }, + title: { + fontSize: 24, + fontWeight: 'bold', + }, + body: { + color: '#4b5563', + fontSize: 15, + marginTop: 10, + }, +}); + +export default JoinPrivateGameScreen; diff --git a/src/components/screens/LeaderboardScreen.js b/src/components/screens/LeaderboardScreen.js new file mode 100644 index 0000000..79ff6a4 --- /dev/null +++ b/src/components/screens/LeaderboardScreen.js @@ -0,0 +1,19 @@ +import React from 'react'; +import {StyleSheet, Text} from 'react-native'; +import Screen from '../layout/Screen'; + +const LeaderboardScreen = () => ( + + Leaderboard + Leaderboard view goes here. + +); + +const styles = StyleSheet.create({ + container: { justifyContent: 'center', alignItems: 'center' }, + title: { fontSize: 24, fontWeight: 'bold' }, + body: { color: '#4b5563', fontSize: 15, marginTop: 10 }, +}); + +export default LeaderboardScreen; + diff --git a/src/components/screens/LoginScreen.js b/src/components/screens/LoginScreen.js new file mode 100644 index 0000000..06172b3 --- /dev/null +++ b/src/components/screens/LoginScreen.js @@ -0,0 +1,110 @@ +import React, {useState} from 'react'; +import {Pressable, StyleSheet, Text, TextInput, View} from 'react-native'; +import useAuthHook from '../../hooks/useAuthHook'; +import Screen from '../layout/Screen'; +import {Button, ButtonTray} from '../UI/Button'; + +const LoginScreen = ({ navigation }) => { + const { email, password, error, isLoading, setEmail, setPassword, login } = useAuthHook(); + const [submitting, setSubmitting] = useState(false); + + const handleLogin = async () => { + setSubmitting(true); + const user = await login(); + setSubmitting(false); + if (!user) { + return; + } + if (user.IsAcceptedAdmin) { + navigation.reset({ index: 0, routes: [{ name: 'Admin' }] }); + return; + } + navigation.reset({ index: 0, routes: [{ name: 'Game' }] }); + }; + + return ( + + + Login + + + + + {error ? {error} : null} + + +