mirror of
https://github.com/mudabbir-ahmad/MAD-Treasure-Hunt.git
synced 2026-10-08 03:00:20 +00:00
Add initial JSON data files and implement core page components for game functionality
This commit is contained in:
1 parent
81847a9188
commit
ca27f8d6ed
54 files changed
+2424
-250
No files matched your search
+215
-34
@@ -1,48 +1,229 @@
|
||||
// This file contains functions to interact with the API or local database.
|
||||
import dbController from '../Model/DbController';
|
||||
|
||||
const API_BASE_URL = 'http://localhost:3000'; // Change this to the actual API endpoint when needed
|
||||
const apiConfig = {
|
||||
useLocalDb: true,
|
||||
baseUrl: 'http://localhost:3000',
|
||||
};
|
||||
|
||||
// Generic fetch function
|
||||
const APIFetch = async (endpoint, options = {}) => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/${endpoint}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
const setApiConfig = (nextConfig = {}) => {
|
||||
Object.assign(apiConfig, nextConfig);
|
||||
};
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API error: ${response.statusText}`);
|
||||
}
|
||||
const isSameId = (item, id) =>
|
||||
item.id?.toString() === id
|
||||
|| item.Uid?.toString() === id
|
||||
|| item.Gid?.toString() === id
|
||||
|| item.SGid?.toString() === id
|
||||
|| item.Tid?.toString() === id;
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Fetch error:', error);
|
||||
throw error;
|
||||
const normalize = (endpoint) => endpoint.replace(/^\/+/, '').split('/');
|
||||
|
||||
const localGet = (endpoint) => {
|
||||
const [resource, id] = normalize(endpoint);
|
||||
const state = dbController.getState();
|
||||
|
||||
if (!resource) {
|
||||
return state;
|
||||
}
|
||||
if (!state[resource]) {
|
||||
throw new Error(`Resource not found: ${resource}`);
|
||||
}
|
||||
if (!id) {
|
||||
return state[resource];
|
||||
}
|
||||
return state[resource].find((item) => isSameId(item, id)) || null;
|
||||
};
|
||||
|
||||
// Example function to fetch a user by username
|
||||
const fetchUserByUsername = async (username) => {
|
||||
return await APIFetch(`users?username=${username}`);
|
||||
const localPost = (endpoint, payload) => {
|
||||
const [resource] = normalize(endpoint);
|
||||
|
||||
if (resource === 'auth' && payload.action === 'login') {
|
||||
return dbController.login(payload.data);
|
||||
}
|
||||
if (resource === 'auth' && payload.action === 'register') {
|
||||
return dbController.register(payload.data);
|
||||
}
|
||||
if (resource === 'groups' && payload.action === 'joinPrivate') {
|
||||
return dbController.joinPrivateGame(payload.data);
|
||||
}
|
||||
if (resource === 'groups' && payload.action === 'joinAsAdmin') {
|
||||
return dbController.joinAsAdmin(payload.data);
|
||||
}
|
||||
if (resource === 'groups' && payload.action === 'createPrivate') {
|
||||
return dbController.createPrivateGame(payload.data);
|
||||
}
|
||||
if (resource === 'groups' && payload.action === 'requestAdmin') {
|
||||
return dbController.requestAdminApproval(payload.data);
|
||||
}
|
||||
if (resource === 'groups' && payload.action === 'approveAdmin') {
|
||||
return dbController.approveAdmin(payload.data);
|
||||
}
|
||||
if (resource === 'subgroups' && payload.action === 'create') {
|
||||
return dbController.createSubgroup(payload.data);
|
||||
}
|
||||
if (resource === 'teams' && payload.action === 'create') {
|
||||
return dbController.createTeam(payload.data);
|
||||
}
|
||||
if (resource === 'teams' && payload.action === 'join') {
|
||||
return dbController.joinTeamByCode(payload.data);
|
||||
}
|
||||
|
||||
const state = dbController.getState();
|
||||
if (!state[resource]) {
|
||||
throw new Error(`Resource not found: ${resource}`);
|
||||
}
|
||||
state[resource].push(payload);
|
||||
return payload;
|
||||
};
|
||||
|
||||
// Example function to fetch all groups
|
||||
const fetchGroups = async () => {
|
||||
return await APIFetch('groups');
|
||||
const localPut = (endpoint, payload) => {
|
||||
const [resource, id] = normalize(endpoint);
|
||||
const state = dbController.getState();
|
||||
if (!state[resource]) {
|
||||
throw new Error(`Resource not found: ${resource}`);
|
||||
}
|
||||
const index = state[resource].findIndex((item) =>
|
||||
isSameId(item, id),
|
||||
);
|
||||
if (index < 0) {
|
||||
throw new Error('Record not found');
|
||||
}
|
||||
state[resource][index] = { ...state[resource][index], ...payload };
|
||||
return state[resource][index];
|
||||
};
|
||||
|
||||
// Example function to fetch subgroups by group ID
|
||||
const fetchSubgroupsByGroupId = async (groupId) => {
|
||||
return await APIFetch(`groups/${groupId}/subgroups`);
|
||||
const localDelete = (endpoint) => {
|
||||
const [resource, id] = normalize(endpoint);
|
||||
const state = dbController.getState();
|
||||
if (!state[resource]) {
|
||||
throw new Error(`Resource not found: ${resource}`);
|
||||
}
|
||||
const index = state[resource].findIndex((item) =>
|
||||
isSameId(item, id),
|
||||
);
|
||||
if (index < 0) {
|
||||
return { deleted: false };
|
||||
}
|
||||
state[resource].splice(index, 1);
|
||||
return { deleted: true };
|
||||
};
|
||||
|
||||
// RESTful API functions
|
||||
const fetchItems = async (endpoint) => await APIFetch(endpoint);
|
||||
const createItem = async (endpoint, data) => await APIFetch(endpoint, { method: 'POST', body: JSON.stringify(data) });
|
||||
const updateItem = async (endpoint, data) => await APIFetch(endpoint, { method: 'PUT', body: JSON.stringify(data) });
|
||||
const deleteItem = async (endpoint) => await APIFetch(endpoint, { method: 'DELETE' });
|
||||
const remoteRequest = async (endpoint, options = {}) => {
|
||||
const response = await fetch(`${apiConfig.baseUrl}/${endpoint.replace(/^\/+/, '')}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(options.headers || {}),
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`API error: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export { APIFetch, fetchUserByUsername, fetchGroups, fetchSubgroupsByGroupId, fetchItems, createItem, updateItem, deleteItem };
|
||||
const APIFetch = async (endpoint) => {
|
||||
if (apiConfig.useLocalDb) {
|
||||
return localGet(endpoint);
|
||||
}
|
||||
return remoteRequest(endpoint, { method: 'GET' });
|
||||
};
|
||||
|
||||
const APIPost = async (endpoint, data) => {
|
||||
if (apiConfig.useLocalDb) {
|
||||
return localPost(endpoint, data);
|
||||
}
|
||||
return remoteRequest(endpoint, { method: 'POST', body: JSON.stringify(data) });
|
||||
};
|
||||
|
||||
const APIPut = async (endpoint, data) => {
|
||||
if (apiConfig.useLocalDb) {
|
||||
if (endpoint === 'groups/settings') {
|
||||
return dbController.updateGroupSettings(data);
|
||||
}
|
||||
if (endpoint === 'subgroups/cache') {
|
||||
return dbController.updateSubgroupCacheSettings(data);
|
||||
}
|
||||
return localPut(endpoint, data);
|
||||
}
|
||||
return remoteRequest(endpoint, { method: 'PUT', body: JSON.stringify(data) });
|
||||
};
|
||||
|
||||
const APIDelete = async (endpoint) => {
|
||||
if (apiConfig.useLocalDb) {
|
||||
return localDelete(endpoint);
|
||||
}
|
||||
return remoteRequest(endpoint, { method: 'DELETE' });
|
||||
};
|
||||
|
||||
const loginUser = async (email, password) => APIPost('auth', { action: 'login', data: { email, password } });
|
||||
const registerUser = async (payload) => APIPost('auth', { action: 'register', data: payload });
|
||||
const getGameTypes = async () => {
|
||||
if (apiConfig.useLocalDb) {
|
||||
return dbController.getGameTypes();
|
||||
}
|
||||
return APIFetch('game_types');
|
||||
};
|
||||
const joinPrivateGame = async (payload) => APIPost('groups', { action: 'joinPrivate', data: payload });
|
||||
const joinAsAdmin = async (payload) => APIPost('groups', { action: 'joinAsAdmin', data: payload });
|
||||
const createPrivateGame = async (payload) => APIPost('groups', { action: 'createPrivate', data: payload });
|
||||
const requestAdminAccess = async (payload) => APIPost('groups', { action: 'requestAdmin', data: payload });
|
||||
const approveAdminAccess = async (payload) => APIPost('groups', { action: 'approveAdmin', data: payload });
|
||||
const createSubgroup = async (payload) => APIPost('subgroups', { action: 'create', data: payload });
|
||||
const getSubgroups = async (gid) => {
|
||||
if (apiConfig.useLocalDb) {
|
||||
return dbController.getSubgroups(gid);
|
||||
}
|
||||
return APIFetch(`groups/${gid}/subgroups`);
|
||||
};
|
||||
const updateSubgroupCache = async (payload) => APIPut('subgroups/cache', payload);
|
||||
const updateGroupSettings = async (payload) => APIPut('groups/settings', payload);
|
||||
const createTeam = async (payload) => APIPost('teams', { action: 'create', data: payload });
|
||||
const joinTeam = async (payload) => APIPost('teams', { action: 'join', data: payload });
|
||||
const getLobby = async (gid) => {
|
||||
if (apiConfig.useLocalDb) {
|
||||
return dbController.getLobby(gid);
|
||||
}
|
||||
return APIFetch(`groups/${gid}/lobby`);
|
||||
};
|
||||
const getTeam = async (tid) => {
|
||||
if (apiConfig.useLocalDb) {
|
||||
return dbController.getTeam(tid);
|
||||
}
|
||||
return APIFetch(`teams/${tid}`);
|
||||
};
|
||||
const getMapPoints = async (gid, sgid = null) => {
|
||||
if (apiConfig.useLocalDb) {
|
||||
return dbController.getMapPoints(gid, sgid);
|
||||
}
|
||||
if (sgid === null || sgid === undefined) {
|
||||
return APIFetch(`groups/${gid}/map`);
|
||||
}
|
||||
return APIFetch(`groups/${gid}/map/${sgid}`);
|
||||
};
|
||||
|
||||
export {
|
||||
apiConfig,
|
||||
setApiConfig,
|
||||
APIFetch,
|
||||
APIPost,
|
||||
APIPut,
|
||||
APIDelete,
|
||||
loginUser,
|
||||
registerUser,
|
||||
getGameTypes,
|
||||
joinPrivateGame,
|
||||
joinAsAdmin,
|
||||
createPrivateGame,
|
||||
requestAdminAccess,
|
||||
approveAdminAccess,
|
||||
createSubgroup,
|
||||
getSubgroups,
|
||||
updateSubgroupCache,
|
||||
updateGroupSettings,
|
||||
createTeam,
|
||||
joinTeam,
|
||||
getLobby,
|
||||
getTeam,
|
||||
getMapPoints,
|
||||
};
|
||||
@@ -1,59 +1,32 @@
|
||||
// If a user's subgroupID is 0, Treat them as ADMIN
|
||||
// If a user's subgroupID is 1, Treat them as USER [For Public instances only subgroups avail are 0 and 1]
|
||||
// If a user's subgroupID is 2, Treat them as USER [For Private instances, subgroups could range from 0-999 where 0 = admin, non 0 = member group]
|
||||
// Use GroupID to link Subgroup to a group in the groups table, where subgroup --> defines permissions and whether the person is allowed to play or not.
|
||||
// within subgroups have TeamGroups --> TeamGroups can come and go, they are not a fixed table [Team groups are enabled per admin request.]
|
||||
// Team group ==> IF Teams are enabled a user can create a team group, if they're admin in team group they can kick people.
|
||||
// All team groups have a unique JOIN CODE.
|
||||
// User's table should have their [NULLABLE] unique Gid, SGid and TGid.
|
||||
// Gid == GroupID || SGid == SubgroupID || TGid == TeamGroupID
|
||||
// When a user logs in, we check their Gid, SGid and TGid to determine their permissions and what they can see on the front end.
|
||||
// If a user is in a team group, they can only see other members of that team group and the games that team group is playing.
|
||||
// If a user is in a subgroup, they can see all members of that subgroup and the games that subgroup is playing.
|
||||
// If a user is in the admin subgroup, they can see all members and all games.
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
||||
const useLoginLogic = () => {
|
||||
const [username, setUsername] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const navigation = useNavigation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
// Replace with your actual API endpoint
|
||||
const response = await fetch('https://yourapi.com/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
alert("API error");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
// Assuming the API returns user data including Gid, SGid, and TGid
|
||||
const { Gid, SGid, TGid } = data;
|
||||
|
||||
// Dispatch user data to Redux store
|
||||
dispatch(setUser({ Gid, SGid, TGid }));
|
||||
|
||||
// Navigate to the appropriate screen based on user role
|
||||
if (SGid === 0) {
|
||||
navigation.navigate('AdminDashboard');
|
||||
} else if (SGid === 1) {
|
||||
navigation.navigate('UserDashboard');
|
||||
} else {
|
||||
navigation.navigate('PrivateDashboard');
|
||||
}
|
||||
} catch (error) {
|
||||
Alert.alert('Login Error', error.message);
|
||||
if (!email || !password) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (email.trim().toLowerCase().includes('admin')) {
|
||||
navigation.navigate('admin-hub');
|
||||
return true;
|
||||
}
|
||||
|
||||
navigation.navigate('game-type');
|
||||
return true;
|
||||
};
|
||||
|
||||
export default LoginLogic;
|
||||
return {
|
||||
email,
|
||||
password,
|
||||
setEmail,
|
||||
setPassword,
|
||||
handleLogin,
|
||||
};
|
||||
};
|
||||
|
||||
export default useLoginLogic;
|
||||
@@ -0,0 +1,502 @@
|
||||
import usersSeed from '../../DB/users.json';
|
||||
import groupsSeed from '../../DB/groups.json';
|
||||
import subgroupsSeed from '../../DB/subgroups.json';
|
||||
import subgroupMembershipsSeed from '../../DB/subgroup-memberships.json';
|
||||
import adminWaitlistSeed from '../../DB/admin-waitlist.json';
|
||||
import teamsSeed from '../../DB/teams.json';
|
||||
import teamMembersSeed from '../../DB/team-members.json';
|
||||
import gameDataSeed from '../../DB/game-data.json';
|
||||
import gameTypesSeed from '../../DB/game-types.json';
|
||||
import UserModel from './UserModel';
|
||||
import GroupModel from './GroupModel';
|
||||
import TeamModel from './TeamModel';
|
||||
|
||||
const buildSeedState = () => ({
|
||||
users: usersSeed,
|
||||
groups: groupsSeed,
|
||||
subgroups: subgroupsSeed,
|
||||
subgroup_memberships: subgroupMembershipsSeed,
|
||||
admin_waitlist: adminWaitlistSeed,
|
||||
teams: teamsSeed,
|
||||
team_members: teamMembersSeed,
|
||||
game_data: gameDataSeed,
|
||||
game_types: gameTypesSeed,
|
||||
});
|
||||
|
||||
class DbController {
|
||||
constructor(seed = buildSeedState()) {
|
||||
this.seed = JSON.parse(JSON.stringify(seed));
|
||||
this.reset();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.db = JSON.parse(JSON.stringify(this.seed));
|
||||
}
|
||||
|
||||
hashPassword(value) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < value.length; i += 1) {
|
||||
hash = (hash << 5) - hash + value.charCodeAt(i);
|
||||
hash |= 0;
|
||||
}
|
||||
return `H_${Math.abs(hash)}`;
|
||||
}
|
||||
|
||||
toMinuteIso(date = new Date()) {
|
||||
return `${date.toISOString().slice(0, 16)}Z`;
|
||||
}
|
||||
|
||||
generateJoinCode(length = 5) {
|
||||
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
let code = '';
|
||||
for (let i = 0; i < length; i += 1) {
|
||||
code += charset[Math.floor(Math.random() * charset.length)];
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
generateUniqueJoinCode(collection, key) {
|
||||
let code = this.generateJoinCode(Math.random() > 0.5 ? 5 : 6);
|
||||
while (collection.some((item) => item[key] === code)) {
|
||||
code = this.generateJoinCode(Math.random() > 0.5 ? 5 : 6);
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
nextId(counterKey) {
|
||||
const keyToCollection = {
|
||||
user: { collection: 'users', field: 'Uid' },
|
||||
group: { collection: 'groups', field: 'Gid' },
|
||||
team: { collection: 'teams', field: 'Tid' },
|
||||
};
|
||||
|
||||
const config = keyToCollection[counterKey];
|
||||
if (!config) {
|
||||
throw new Error(`Unsupported counter key: ${counterKey}`);
|
||||
}
|
||||
|
||||
const rows = this.db[config.collection] || [];
|
||||
const maxId = rows.reduce((max, row) => {
|
||||
const value = Number(row[config.field] || 0);
|
||||
return value > max ? value : max;
|
||||
}, 0);
|
||||
|
||||
return maxId + 1;
|
||||
}
|
||||
|
||||
getUserByEmail(email) {
|
||||
return this.db.users.find((u) => u.email.toLowerCase() === email.toLowerCase()) || null;
|
||||
}
|
||||
|
||||
getUserById(uid) {
|
||||
return this.db.users.find((u) => u.Uid === Number(uid)) || null;
|
||||
}
|
||||
|
||||
getGroupById(gid) {
|
||||
return this.db.groups.find((group) => group.Gid === Number(gid)) || null;
|
||||
}
|
||||
|
||||
register({ username, email, password, confirmPassword, accountType }) {
|
||||
if (!username || !email || !password || !confirmPassword) {
|
||||
throw new Error('Missing required registration fields');
|
||||
}
|
||||
if (password !== confirmPassword) {
|
||||
throw new Error('Passwords do not match');
|
||||
}
|
||||
if (this.getUserByEmail(email)) {
|
||||
throw new Error('Email already in use');
|
||||
}
|
||||
|
||||
const uid = this.nextId('user');
|
||||
const user = new UserModel({
|
||||
Uid: uid,
|
||||
username,
|
||||
email,
|
||||
passwordHash: this.hashPassword(password),
|
||||
isBusiness: accountType === 'Business/School',
|
||||
IsAcceptedAdmin: false,
|
||||
Gid: null,
|
||||
SGid: null,
|
||||
TGid: null,
|
||||
});
|
||||
|
||||
this.db.users.push(user.toJSON());
|
||||
return user.toJSON();
|
||||
}
|
||||
|
||||
login({ email, password }) {
|
||||
const user = this.getUserByEmail(email);
|
||||
if (!user) {
|
||||
throw new Error('Email not found');
|
||||
}
|
||||
const incomingHash = this.hashPassword(password);
|
||||
if (incomingHash !== user.passwordHash) {
|
||||
throw new Error('Invalid password');
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
getGameTypes() {
|
||||
return this.db.game_types;
|
||||
}
|
||||
|
||||
getSubgroupByJoinCode(joinCode) {
|
||||
return this.db.subgroups.find(
|
||||
(subgroup) => subgroup.JoinCode && subgroup.JoinCode === joinCode.toUpperCase(),
|
||||
) || null;
|
||||
}
|
||||
|
||||
ensureMembership(uid, gid, sgid, isAcceptedAdmin) {
|
||||
const existing = this.db.subgroup_memberships.find(
|
||||
(membership) => membership.Uid === uid && membership.Gid === gid,
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
existing.SGid = sgid;
|
||||
existing.IsAcceptedAdmin = isAcceptedAdmin;
|
||||
return existing;
|
||||
}
|
||||
|
||||
const membership = {
|
||||
Uid: uid,
|
||||
Gid: gid,
|
||||
SGid: sgid,
|
||||
IsAcceptedAdmin: isAcceptedAdmin,
|
||||
};
|
||||
this.db.subgroup_memberships.push(membership);
|
||||
return membership;
|
||||
}
|
||||
|
||||
ensureGroupGameData(gid) {
|
||||
const existing = this.db.game_data.find((entry) => entry.Gid === gid);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
const payload = { Gid: gid, GeoCaches: [] };
|
||||
this.db.game_data.push(payload);
|
||||
return payload;
|
||||
}
|
||||
|
||||
hasActiveAdmin(gid) {
|
||||
return this.db.subgroup_memberships.some(
|
||||
(membership) => membership.Gid === gid && membership.SGid === 0 && membership.IsAcceptedAdmin,
|
||||
);
|
||||
}
|
||||
|
||||
canJoinTeamForSubgroup(group, sgid) {
|
||||
return group.TeamsEnabled && sgid > 0;
|
||||
}
|
||||
|
||||
joinPrivateGame({ userId, joinCode }) {
|
||||
const user = this.getUserById(Number(userId));
|
||||
const subgroup = this.getSubgroupByJoinCode(joinCode);
|
||||
|
||||
if (!user || !subgroup || subgroup.IsAdminGroup) {
|
||||
throw new Error('Invalid subgroup join code');
|
||||
}
|
||||
|
||||
const group = this.getGroupById(subgroup.Gid);
|
||||
if (!group) {
|
||||
throw new Error('Group not found');
|
||||
}
|
||||
|
||||
this.ensureMembership(user.Uid, subgroup.Gid, subgroup.SGid, false);
|
||||
user.Gid = group.Gid;
|
||||
user.SGid = subgroup.SGid;
|
||||
user.IsAcceptedAdmin = false;
|
||||
return group;
|
||||
}
|
||||
|
||||
createPrivateGame({ userId, groupName, businessOrSchoolName, gameType = 'private', isBusiness = false }) {
|
||||
const user = this.getUserById(Number(userId));
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
const gid = this.nextId('group');
|
||||
const group = new GroupModel({
|
||||
Gid: gid,
|
||||
GroupName: groupName,
|
||||
BusinessOrSchoolName: businessOrSchoolName || groupName,
|
||||
GameType: gameType,
|
||||
AdminJoinCode: this.generateUniqueJoinCode(this.db.groups, 'AdminJoinCode'),
|
||||
CreatedByUid: user.Uid,
|
||||
MaxMemberSubgroups: isBusiness ? 999 : 1,
|
||||
TeamsEnabled: true,
|
||||
ApprovedAdmins: [user.Uid],
|
||||
CreatedAt: this.toMinuteIso(),
|
||||
});
|
||||
|
||||
this.db.groups.push(group.toJSON());
|
||||
this.ensureGroupGameData(gid);
|
||||
|
||||
this.db.subgroups.push({
|
||||
SGid: 0,
|
||||
SubGroupName: 'Admins',
|
||||
Gid: group.Gid,
|
||||
JoinCode: null,
|
||||
IsAdminGroup: true,
|
||||
CacheTriggerMeters: 20,
|
||||
});
|
||||
|
||||
this.createSubgroup({ gid: group.Gid, subgroupName: 'Members', cacheTriggerMeters: 20 });
|
||||
|
||||
this.ensureMembership(user.Uid, group.Gid, 0, true);
|
||||
user.Gid = group.Gid;
|
||||
user.SGid = 0;
|
||||
user.IsAcceptedAdmin = true;
|
||||
|
||||
return group.toJSON();
|
||||
}
|
||||
|
||||
joinAsAdmin({ userId, businessOrSchoolName }) {
|
||||
const user = this.getUserById(Number(userId));
|
||||
const group = this.db.groups.find(
|
||||
(entry) => entry.BusinessOrSchoolName.toLowerCase() === businessOrSchoolName.trim().toLowerCase(),
|
||||
);
|
||||
|
||||
if (!user || !group) {
|
||||
throw new Error('User or business/school not found');
|
||||
}
|
||||
|
||||
this.ensureMembership(user.Uid, group.Gid, 0, false);
|
||||
user.Gid = group.Gid;
|
||||
user.SGid = 0;
|
||||
user.IsAcceptedAdmin = false;
|
||||
|
||||
const activeAdminExists = this.hasActiveAdmin(group.Gid);
|
||||
if (!activeAdminExists) {
|
||||
return this.promoteAdmin({ uid: user.Uid, gid: group.Gid, approvedByUid: user.Uid });
|
||||
}
|
||||
|
||||
const alreadyQueued = this.db.admin_waitlist.some(
|
||||
(entry) => entry.Uid === user.Uid && entry.Gid === group.Gid && entry.Status === 'pending',
|
||||
);
|
||||
if (!alreadyQueued) {
|
||||
this.db.admin_waitlist.push({
|
||||
RequestId: `${group.Gid}_${user.Uid}`,
|
||||
Uid: user.Uid,
|
||||
Gid: group.Gid,
|
||||
RequestedAt: this.toMinuteIso(),
|
||||
Status: 'pending',
|
||||
});
|
||||
}
|
||||
|
||||
return { queued: true, Gid: group.Gid, Uid: user.Uid };
|
||||
}
|
||||
|
||||
promoteAdmin({ uid, gid, approvedByUid }) {
|
||||
const user = this.getUserById(Number(uid));
|
||||
const group = this.getGroupById(Number(gid));
|
||||
if (!user || !group) {
|
||||
throw new Error('User or group not found');
|
||||
}
|
||||
|
||||
this.ensureMembership(user.Uid, group.Gid, 0, true);
|
||||
user.Gid = group.Gid;
|
||||
user.SGid = 0;
|
||||
user.IsAcceptedAdmin = true;
|
||||
|
||||
if (!group.ApprovedAdmins.includes(user.Uid)) {
|
||||
group.ApprovedAdmins.push(user.Uid);
|
||||
}
|
||||
if (!group.ApprovedAdmins.includes(Number(approvedByUid))) {
|
||||
group.ApprovedAdmins.push(Number(approvedByUid));
|
||||
}
|
||||
|
||||
this.db.admin_waitlist = this.db.admin_waitlist.map((entry) => {
|
||||
if (entry.Uid === user.Uid && entry.Gid === group.Gid && entry.Status === 'pending') {
|
||||
return { ...entry, Status: 'approved' };
|
||||
}
|
||||
return entry;
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
requestAdminApproval({ userId, groupId }) {
|
||||
const group = this.getGroupById(Number(groupId));
|
||||
if (!group) {
|
||||
throw new Error('Group not found');
|
||||
}
|
||||
return this.joinAsAdmin({ userId, businessOrSchoolName: group.BusinessOrSchoolName });
|
||||
}
|
||||
|
||||
approveAdmin({ approverUserId, userId, groupId }) {
|
||||
const approverMembership = this.db.subgroup_memberships.find(
|
||||
(membership) => membership.Uid === Number(approverUserId)
|
||||
&& membership.Gid === Number(groupId)
|
||||
&& membership.SGid === 0
|
||||
&& membership.IsAcceptedAdmin,
|
||||
);
|
||||
if (!approverMembership) {
|
||||
throw new Error('Approver is not an admin for this group');
|
||||
}
|
||||
return this.promoteAdmin({ uid: Number(userId), gid: Number(groupId), approvedByUid: Number(approverUserId) });
|
||||
}
|
||||
|
||||
createSubgroup({ gid, subgroupName, cacheTriggerMeters = 20 }) {
|
||||
const group = this.getGroupById(Number(gid));
|
||||
if (!group) {
|
||||
throw new Error('Group not found');
|
||||
}
|
||||
|
||||
const nonAdminCount = this.db.subgroups.filter((entry) => entry.Gid === group.Gid && !entry.IsAdminGroup).length;
|
||||
if (nonAdminCount >= group.MaxMemberSubgroups) {
|
||||
throw new Error('Maximum subgroup count reached');
|
||||
}
|
||||
|
||||
const nextSgid = this.db.subgroups
|
||||
.filter((entry) => entry.Gid === group.Gid && !entry.IsAdminGroup)
|
||||
.reduce((max, entry) => Math.max(max, entry.SGid), 0) + 1;
|
||||
|
||||
const subgroup = {
|
||||
SGid: nextSgid,
|
||||
SubGroupName: subgroupName,
|
||||
Gid: group.Gid,
|
||||
JoinCode: this.generateUniqueJoinCode(this.db.subgroups.filter((entry) => !entry.IsAdminGroup), 'JoinCode'),
|
||||
IsAdminGroup: false,
|
||||
CacheTriggerMeters: cacheTriggerMeters,
|
||||
};
|
||||
|
||||
this.db.subgroups.push(subgroup);
|
||||
return subgroup;
|
||||
}
|
||||
|
||||
getSubgroups(gid) {
|
||||
return this.db.subgroups.filter((subgroup) => subgroup.Gid === Number(gid));
|
||||
}
|
||||
|
||||
updateSubgroupCacheSettings({ gid, sgid, triggerMeters }) {
|
||||
const subgroup = this.db.subgroups.find(
|
||||
(entry) => entry.Gid === Number(gid) && entry.SGid === Number(sgid),
|
||||
);
|
||||
if (!subgroup) {
|
||||
throw new Error('Subgroup not found');
|
||||
}
|
||||
subgroup.CacheTriggerMeters = Number(triggerMeters);
|
||||
return subgroup;
|
||||
}
|
||||
|
||||
createTeam({ userId, gid, teamName }) {
|
||||
const user = this.getUserById(Number(userId));
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
const group = this.getGroupById(Number(gid));
|
||||
if (!group) {
|
||||
throw new Error('Group not found');
|
||||
}
|
||||
if (!this.canJoinTeamForSubgroup(group, user.SGid || 0)) {
|
||||
throw new Error('Teams are disabled or user is in admin subgroup');
|
||||
}
|
||||
|
||||
const tid = this.nextId('team');
|
||||
const team = new TeamModel({
|
||||
Tid: tid,
|
||||
TeamName: teamName,
|
||||
Gid: group.Gid,
|
||||
JoinCode: this.generateUniqueJoinCode(this.db.teams, 'JoinCode'),
|
||||
CreatorUid: user.Uid,
|
||||
SGid: user.SGid,
|
||||
CreatedAt: this.toMinuteIso(),
|
||||
});
|
||||
|
||||
this.db.teams.push(team.toJSON());
|
||||
this.db.team_members.push({
|
||||
Tid: team.Tid,
|
||||
Uid: user.Uid,
|
||||
IsTeamCreator: true,
|
||||
});
|
||||
|
||||
user.TGid = team.Tid;
|
||||
return team.toJSON();
|
||||
}
|
||||
|
||||
joinTeamByCode({ userId, teamCode }) {
|
||||
const user = this.getUserById(Number(userId));
|
||||
const team = this.db.teams.find((t) => t.JoinCode === teamCode.toUpperCase());
|
||||
if (!user || !team) {
|
||||
throw new Error('Invalid user or team code');
|
||||
}
|
||||
|
||||
const group = this.getGroupById(team.Gid);
|
||||
if (!group || !this.canJoinTeamForSubgroup(group, user.SGid || 0)) {
|
||||
throw new Error('Teams are disabled or user is in admin subgroup');
|
||||
}
|
||||
|
||||
if (user.Gid !== team.Gid) {
|
||||
throw new Error('User is not in this game group');
|
||||
}
|
||||
|
||||
const alreadyInTeam = this.db.team_members.some((tm) => tm.Tid === team.Tid && tm.Uid === user.Uid);
|
||||
if (!alreadyInTeam) {
|
||||
this.db.team_members.push({ Tid: team.Tid, Uid: user.Uid, IsTeamCreator: false });
|
||||
}
|
||||
|
||||
user.TGid = team.Tid;
|
||||
return team;
|
||||
}
|
||||
|
||||
getLobby(gid) {
|
||||
const group = this.getGroupById(Number(gid));
|
||||
if (!group) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const members = this.db.subgroup_memberships
|
||||
.filter((membership) => membership.Gid === group.Gid)
|
||||
.map((membership) => this.getUserById(membership.Uid))
|
||||
.filter(Boolean);
|
||||
|
||||
return {
|
||||
group,
|
||||
members,
|
||||
subgroups: this.getSubgroups(group.Gid),
|
||||
teams: this.db.teams.filter((team) => team.Gid === group.Gid),
|
||||
};
|
||||
}
|
||||
|
||||
getTeam(tid) {
|
||||
const team = this.db.teams.find((entry) => entry.Tid === Number(tid));
|
||||
if (!team) {
|
||||
return null;
|
||||
}
|
||||
const members = this.db.team_members
|
||||
.filter((member) => member.Tid === team.Tid)
|
||||
.map((member) => this.getUserById(member.Uid))
|
||||
.filter(Boolean);
|
||||
return { team, members };
|
||||
}
|
||||
|
||||
getMapPoints(gid, sgid = null) {
|
||||
const gameData = this.db.game_data.find((entry) => entry.Gid === Number(gid));
|
||||
if (!gameData) {
|
||||
return [];
|
||||
}
|
||||
if (sgid === null || sgid === undefined) {
|
||||
return gameData.GeoCaches;
|
||||
}
|
||||
return gameData.GeoCaches.filter((cache) => cache.SGid === Number(sgid));
|
||||
}
|
||||
|
||||
updateGroupSettings({ gid, teamsEnabled }) {
|
||||
const group = this.getGroupById(Number(gid));
|
||||
if (!group) {
|
||||
throw new Error('Group not found');
|
||||
}
|
||||
group.TeamsEnabled = Boolean(teamsEnabled);
|
||||
return group;
|
||||
}
|
||||
|
||||
getState() {
|
||||
return this.db;
|
||||
}
|
||||
}
|
||||
|
||||
const dbController = new DbController();
|
||||
|
||||
export { DbController };
|
||||
export default dbController;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
class GroupModel {
|
||||
constructor(payload) {
|
||||
this.Gid = payload.Gid;
|
||||
this.GroupName = payload.GroupName;
|
||||
this.BusinessOrSchoolName = payload.BusinessOrSchoolName;
|
||||
this.GameType = payload.GameType;
|
||||
this.AdminJoinCode = payload.AdminJoinCode;
|
||||
this.CreatedByUid = payload.CreatedByUid;
|
||||
this.MaxMemberSubgroups = payload.MaxMemberSubgroups;
|
||||
this.TeamsEnabled = Boolean(payload.TeamsEnabled);
|
||||
this.ApprovedAdmins = payload.ApprovedAdmins || [];
|
||||
this.CreatedAt = payload.CreatedAt;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
Gid: this.Gid,
|
||||
GroupName: this.GroupName,
|
||||
BusinessOrSchoolName: this.BusinessOrSchoolName,
|
||||
GameType: this.GameType,
|
||||
AdminJoinCode: this.AdminJoinCode,
|
||||
CreatedByUid: this.CreatedByUid,
|
||||
MaxMemberSubgroups: this.MaxMemberSubgroups,
|
||||
TeamsEnabled: this.TeamsEnabled,
|
||||
ApprovedAdmins: this.ApprovedAdmins,
|
||||
CreatedAt: this.CreatedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default GroupModel;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
const session = {
|
||||
currentUid: null,
|
||||
currentGid: null,
|
||||
currentSGid: null,
|
||||
currentTGid: null,
|
||||
};
|
||||
|
||||
const setSessionUser = (user) => {
|
||||
session.currentUid = user?.Uid ?? null;
|
||||
session.currentGid = user?.Gid ?? null;
|
||||
session.currentSGid = user?.SGid ?? null;
|
||||
session.currentTGid = user?.TGid ?? null;
|
||||
};
|
||||
|
||||
const setSessionGroup = (gid, sgid = null) => {
|
||||
session.currentGid = gid ?? null;
|
||||
if (sgid !== null && sgid !== undefined) {
|
||||
session.currentSGid = sgid;
|
||||
}
|
||||
};
|
||||
|
||||
const setSessionTeam = (tgid) => {
|
||||
session.currentTGid = tgid ?? null;
|
||||
};
|
||||
|
||||
const getSession = () => ({ ...session });
|
||||
|
||||
export { setSessionUser, setSessionGroup, setSessionTeam, getSession };
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
class TeamModel {
|
||||
constructor(payload) {
|
||||
this.Tid = payload.Tid;
|
||||
this.TeamName = payload.TeamName;
|
||||
this.Gid = payload.Gid;
|
||||
this.JoinCode = payload.JoinCode;
|
||||
this.CreatorUid = payload.CreatorUid;
|
||||
this.SGid = payload.SGid;
|
||||
this.CreatedAt = payload.CreatedAt;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
Tid: this.Tid,
|
||||
TeamName: this.TeamName,
|
||||
Gid: this.Gid,
|
||||
JoinCode: this.JoinCode,
|
||||
CreatorUid: this.CreatorUid,
|
||||
SGid: this.SGid,
|
||||
CreatedAt: this.CreatedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default TeamModel;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
class UserModel {
|
||||
constructor(payload) {
|
||||
this.Uid = payload.Uid;
|
||||
this.username = payload.username;
|
||||
this.email = payload.email;
|
||||
this.passwordHash = payload.passwordHash;
|
||||
this.isBusiness = Boolean(payload.isBusiness);
|
||||
this.IsAcceptedAdmin = Boolean(payload.IsAcceptedAdmin);
|
||||
this.Gid = payload.Gid ?? null;
|
||||
this.SGid = payload.SGid ?? null;
|
||||
this.TGid = payload.TGid ?? null;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
Uid: this.Uid,
|
||||
username: this.username,
|
||||
email: this.email,
|
||||
passwordHash: this.passwordHash,
|
||||
isBusiness: this.isBusiness,
|
||||
IsAcceptedAdmin: this.IsAcceptedAdmin,
|
||||
Gid: this.Gid,
|
||||
SGid: this.SGid,
|
||||
TGid: this.TGid,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default UserModel;
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import { View, Text, Pressable, StyleSheet } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import AdminFooterNavbar from '../components/AdminFooterNavbar';
|
||||
import useAdminViewModel from '../ViewModel/useAdminViewModel';
|
||||
|
||||
const AdminHubPage = () => {
|
||||
const navigation = useNavigation();
|
||||
const { createGame } = useAdminViewModel();
|
||||
|
||||
const handleCreateGame = async () => {
|
||||
await createGame({
|
||||
groupName: `Admin Game ${Date.now().toString().slice(-4)}`,
|
||||
businessOrSchoolName: 'New Business',
|
||||
gameType: 'private',
|
||||
isBusiness: true,
|
||||
});
|
||||
navigation.navigate('game-lobby');
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.wrapper}>
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Admin Hub</Text>
|
||||
<Pressable style={styles.button} onPress={handleCreateGame}>
|
||||
<Text style={styles.buttonText}>Create a Game</Text>
|
||||
</Pressable>
|
||||
<Pressable style={styles.button} onPress={() => navigation.navigate('game-lobby')}>
|
||||
<Text style={styles.buttonText}>Manage Existing Game</Text>
|
||||
</Pressable>
|
||||
<Pressable style={styles.button} onPress={() => navigation.navigate('manage-subgroups')}>
|
||||
<Text style={styles.buttonText}>Manage Subgroups</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
<AdminFooterNavbar />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
wrapper: {
|
||||
flex: 1,
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
padding: 20,
|
||||
gap: 12,
|
||||
},
|
||||
title: {
|
||||
fontSize: 26,
|
||||
fontWeight: '700',
|
||||
color: '#111827',
|
||||
marginBottom: 8,
|
||||
},
|
||||
button: {
|
||||
backgroundColor: '#2563eb',
|
||||
borderRadius: 8,
|
||||
paddingVertical: 12,
|
||||
alignItems: 'center',
|
||||
},
|
||||
buttonText: {
|
||||
color: '#ffffff',
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
export default AdminHubPage;
|
||||
|
||||
@@ -1,15 +1,105 @@
|
||||
import React from 'react';
|
||||
import { View, Text, Button, TextInput } from 'react-native';
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, Pressable, TextInput, StyleSheet } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import MemberFooterNavbar from '../components/MemberFooterNavbar';
|
||||
import useLobbyViewModel from '../ViewModel/useLobbyViewModel';
|
||||
|
||||
const GameLobbyPage = () => {
|
||||
const navigation = useNavigation();
|
||||
const { teamCode, error, setTeamCode, createLobbyTeam, joinLobbyTeam } = useLobbyViewModel();
|
||||
const [hasTeam, setHasTeam] = useState(false);
|
||||
|
||||
const handleCreateTeam = async () => {
|
||||
const team = await createLobbyTeam({
|
||||
teamName: `Team-${Date.now().toString().slice(-4)}`,
|
||||
});
|
||||
if (!team) {
|
||||
return;
|
||||
}
|
||||
setHasTeam(true);
|
||||
navigation.navigate('team');
|
||||
};
|
||||
|
||||
const handleJoinTeam = async () => {
|
||||
const team = await joinLobbyTeam();
|
||||
if (!team) {
|
||||
return;
|
||||
}
|
||||
setHasTeam(true);
|
||||
navigation.navigate('team');
|
||||
};
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Text>Game Lobby</Text>
|
||||
<Button title="Create Team" onPress={() => {}} />
|
||||
<Text>Join Team Code:</Text>
|
||||
<TextInput placeholder="Enter Team Join Code" />
|
||||
<View style={styles.wrapper}>
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Game Lobby</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Join Team Code"
|
||||
autoCapitalize="characters"
|
||||
maxLength={6}
|
||||
value={teamCode}
|
||||
onChangeText={setTeamCode}
|
||||
/>
|
||||
<Pressable style={styles.secondaryButton} onPress={handleJoinTeam}>
|
||||
<Text style={styles.buttonText}>Join Team</Text>
|
||||
</Pressable>
|
||||
<Pressable style={styles.primaryButton} onPress={handleCreateTeam}>
|
||||
<Text style={styles.buttonText}>Create Team</Text>
|
||||
</Pressable>
|
||||
<Pressable style={styles.secondaryButton} onPress={() => navigation.navigate('map')}>
|
||||
<Text style={styles.buttonText}>Open Map</Text>
|
||||
</Pressable>
|
||||
{!!error && <Text style={styles.errorText}>{error}</Text>}
|
||||
</View>
|
||||
<MemberFooterNavbar visible={hasTeam} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
wrapper: {
|
||||
flex: 1,
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
padding: 20,
|
||||
gap: 12,
|
||||
},
|
||||
title: {
|
||||
fontSize: 26,
|
||||
fontWeight: '700',
|
||||
color: '#111827',
|
||||
marginBottom: 8,
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#d1d5db',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
primaryButton: {
|
||||
backgroundColor: '#2563eb',
|
||||
borderRadius: 8,
|
||||
paddingVertical: 12,
|
||||
alignItems: 'center',
|
||||
},
|
||||
secondaryButton: {
|
||||
backgroundColor: '#4b5563',
|
||||
borderRadius: 8,
|
||||
paddingVertical: 12,
|
||||
alignItems: 'center',
|
||||
},
|
||||
buttonText: {
|
||||
color: '#ffffff',
|
||||
fontWeight: '600',
|
||||
},
|
||||
errorText: {
|
||||
color: '#dc2626',
|
||||
},
|
||||
});
|
||||
|
||||
export default GameLobbyPage;
|
||||
@@ -1,14 +1,60 @@
|
||||
import React from 'react';
|
||||
import { View, Button } from 'react-native';
|
||||
import { View, Text, Pressable, StyleSheet } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import useGameTypeViewModel from '../ViewModel/useGameTypeViewModel';
|
||||
|
||||
const GameTypePage = () => {
|
||||
const navigation = useNavigation();
|
||||
const { gameTypes } = useGameTypeViewModel();
|
||||
|
||||
const navigateByType = (label) => {
|
||||
if (label === 'Join Global Game') {
|
||||
navigation.navigate('game-lobby');
|
||||
return;
|
||||
}
|
||||
if (label === 'Join Private Game') {
|
||||
navigation.navigate('join-private-game');
|
||||
return;
|
||||
}
|
||||
navigation.navigate('manage-private-subtask');
|
||||
};
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Button title="Join Global Game" onPress={() => {}} />
|
||||
<Button title="Join Private Game" onPress={() => {}} />
|
||||
<Button title="Manage Private Game" onPress={() => {}} />
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Select Game Type</Text>
|
||||
{gameTypes.map((label) => (
|
||||
<Pressable key={label} style={styles.button} onPress={() => navigateByType(label)}>
|
||||
<Text style={styles.buttonText}>{label}</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
padding: 20,
|
||||
gap: 12,
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
title: {
|
||||
fontSize: 26,
|
||||
fontWeight: '700',
|
||||
color: '#111827',
|
||||
marginBottom: 8,
|
||||
},
|
||||
button: {
|
||||
backgroundColor: '#2563eb',
|
||||
borderRadius: 8,
|
||||
paddingVertical: 12,
|
||||
alignItems: 'center',
|
||||
},
|
||||
buttonText: {
|
||||
color: '#ffffff',
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
export default GameTypePage;
|
||||
@@ -1,19 +1,76 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, TextInput, Button } from 'react-native';
|
||||
import { View, Text, TextInput, Pressable, StyleSheet } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import usePrivateGameViewModel from '../ViewModel/usePrivateGameViewModel';
|
||||
|
||||
const JoinPrivateGamePage = () => {
|
||||
const [joinCode, setJoinCode] = useState('');
|
||||
const navigation = useNavigation();
|
||||
const { joinCode, error, setJoinCode, joinWithCode } = usePrivateGameViewModel();
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const handleJoinGame = () => {
|
||||
// Logic to check join code and redirect to game lobby
|
||||
const handleJoinGame = async () => {
|
||||
setSubmitting(true);
|
||||
const group = await joinWithCode();
|
||||
setSubmitting(false);
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
navigation.navigate('game-lobby');
|
||||
};
|
||||
|
||||
return (
|
||||
<View>
|
||||
<TextInput placeholder="Enter Join Code" value={joinCode} onChangeText={setJoinCode} />
|
||||
<Button title="Join Game" onPress={handleJoinGame} />
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Join Private Game</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Enter Join Code"
|
||||
autoCapitalize="characters"
|
||||
maxLength={6}
|
||||
value={joinCode}
|
||||
onChangeText={setJoinCode}
|
||||
/>
|
||||
<Pressable style={styles.button} onPress={handleJoinGame}>
|
||||
<Text style={styles.buttonText}>{submitting ? 'Joining...' : 'Join Game'}</Text>
|
||||
</Pressable>
|
||||
{!!error && <Text style={styles.errorText}>{error}</Text>}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
padding: 20,
|
||||
gap: 12,
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
title: {
|
||||
fontSize: 26,
|
||||
fontWeight: '700',
|
||||
color: '#111827',
|
||||
marginBottom: 8,
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#d1d5db',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
button: {
|
||||
backgroundColor: '#2563eb',
|
||||
borderRadius: 8,
|
||||
paddingVertical: 12,
|
||||
alignItems: 'center',
|
||||
},
|
||||
buttonText: {
|
||||
color: '#ffffff',
|
||||
fontWeight: '600',
|
||||
},
|
||||
errorText: {
|
||||
color: '#dc2626',
|
||||
},
|
||||
});
|
||||
|
||||
export default JoinPrivateGamePage;
|
||||
@@ -0,0 +1,114 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, TextInput, Pressable, StyleSheet } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import useAuthViewModel from '../ViewModel/useAuthViewModel';
|
||||
|
||||
const LoginPage = () => {
|
||||
const navigation = useNavigation();
|
||||
const {
|
||||
email,
|
||||
password,
|
||||
error,
|
||||
setEmail,
|
||||
setPassword,
|
||||
login,
|
||||
} = useAuthViewModel();
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const handleLogin = async () => {
|
||||
setSubmitting(true);
|
||||
const user = await login();
|
||||
setSubmitting(false);
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
if (user.SGid === 0) {
|
||||
navigation.navigate('admin-hub');
|
||||
return;
|
||||
}
|
||||
navigation.navigate('game-type');
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Login</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Email"
|
||||
autoCapitalize="none"
|
||||
keyboardType="email-address"
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
/>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Password"
|
||||
secureTextEntry
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
/>
|
||||
<View style={styles.row}>
|
||||
<Pressable style={styles.leftButton} onPress={() => navigation.navigate('register')}>
|
||||
<Text style={styles.buttonText}>Register</Text>
|
||||
</Pressable>
|
||||
<Pressable style={styles.rightButton} onPress={handleLogin}>
|
||||
<Text style={styles.buttonText}>{submitting ? 'Logging in...' : 'Login'}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
{!!error && <Text style={styles.errorText}>{error}</Text>}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 20,
|
||||
justifyContent: 'center',
|
||||
gap: 12,
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
title: {
|
||||
fontSize: 28,
|
||||
fontWeight: '700',
|
||||
marginBottom: 8,
|
||||
color: '#111827',
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#d1d5db',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
gap: 10,
|
||||
marginTop: 4,
|
||||
},
|
||||
leftButton: {
|
||||
flex: 1,
|
||||
backgroundColor: '#4b5563',
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
paddingVertical: 12,
|
||||
},
|
||||
rightButton: {
|
||||
flex: 1,
|
||||
backgroundColor: '#2563eb',
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
paddingVertical: 12,
|
||||
},
|
||||
buttonText: {
|
||||
color: '#ffffff',
|
||||
fontWeight: '600',
|
||||
},
|
||||
errorText: {
|
||||
color: '#dc2626',
|
||||
marginTop: 4,
|
||||
},
|
||||
});
|
||||
|
||||
export default LoginPage;
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, TextInput, Pressable, StyleSheet } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import useManagePrivateSubtaskViewModel from '../ViewModel/useManagePrivateSubtaskViewModel';
|
||||
|
||||
const ManagePrivateSubtaskPage = () => {
|
||||
const navigation = useNavigation();
|
||||
const {
|
||||
businessOrSchoolName,
|
||||
error,
|
||||
setBusinessOrSchoolName,
|
||||
createNewPrivateGame,
|
||||
joinAdminForPrivate,
|
||||
} = useManagePrivateSubtaskViewModel();
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const handleCreate = async () => {
|
||||
setSubmitting(true);
|
||||
const group = await createNewPrivateGame();
|
||||
setSubmitting(false);
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
navigation.navigate('admin-hub');
|
||||
};
|
||||
|
||||
const handleJoinAdmin = async () => {
|
||||
setSubmitting(true);
|
||||
const response = await joinAdminForPrivate();
|
||||
setSubmitting(false);
|
||||
if (!response) {
|
||||
return;
|
||||
}
|
||||
navigation.navigate('admin-hub');
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Manage Private Game</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Business/School Name"
|
||||
value={businessOrSchoolName}
|
||||
onChangeText={setBusinessOrSchoolName}
|
||||
/>
|
||||
<Pressable style={styles.primaryButton} onPress={handleCreate}>
|
||||
<Text style={styles.buttonText}>{submitting ? 'Please wait...' : 'Create New Private Game'}</Text>
|
||||
</Pressable>
|
||||
<Pressable style={styles.secondaryButton} onPress={handleJoinAdmin}>
|
||||
<Text style={styles.buttonText}>Join as Admin for Private Game</Text>
|
||||
</Pressable>
|
||||
{!!error && <Text style={styles.errorText}>{error}</Text>}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
gap: 12,
|
||||
padding: 20,
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
title: {
|
||||
fontSize: 26,
|
||||
fontWeight: '700',
|
||||
color: '#111827',
|
||||
marginBottom: 8,
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#d1d5db',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
primaryButton: {
|
||||
backgroundColor: '#2563eb',
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
paddingVertical: 12,
|
||||
},
|
||||
secondaryButton: {
|
||||
backgroundColor: '#4b5563',
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
paddingVertical: 12,
|
||||
},
|
||||
buttonText: {
|
||||
color: '#ffffff',
|
||||
fontWeight: '600',
|
||||
},
|
||||
errorText: {
|
||||
color: '#dc2626',
|
||||
},
|
||||
});
|
||||
|
||||
export default ManagePrivateSubtaskPage;
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import React from 'react';
|
||||
import { View, Text, TextInput, Pressable, StyleSheet } from 'react-native';
|
||||
import useSubgroupAdminViewModel from '../ViewModel/useSubgroupAdminViewModel';
|
||||
import AdminFooterNavbar from '../components/AdminFooterNavbar';
|
||||
|
||||
const ManageSubgroupsPage = () => {
|
||||
const {
|
||||
subgroups,
|
||||
subgroupName,
|
||||
cacheTriggerMeters,
|
||||
error,
|
||||
setSubgroupName,
|
||||
setCacheTriggerMeters,
|
||||
addSubgroup,
|
||||
updateCacheForSubgroup,
|
||||
} = useSubgroupAdminViewModel();
|
||||
|
||||
return (
|
||||
<View style={styles.wrapper}>
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Manage Subgroups</Text>
|
||||
{subgroups.map((subgroup) => (
|
||||
<View key={`${subgroup.Gid}_${subgroup.SGid}`} style={styles.row}>
|
||||
<Text style={styles.rowText}>
|
||||
SGid {subgroup.SGid} | {subgroup.SubGroupName} | Code: {subgroup.JoinCode || 'ADMIN'} | Trigger: {subgroup.CacheTriggerMeters}m
|
||||
</Text>
|
||||
<Pressable
|
||||
style={styles.inlineButton}
|
||||
onPress={() => updateCacheForSubgroup(subgroup.SGid, (subgroup.CacheTriggerMeters || 20) + 5)}
|
||||
>
|
||||
<Text style={styles.inlineButtonText}>+5m</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
))}
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="New subgroup name"
|
||||
value={subgroupName}
|
||||
onChangeText={setSubgroupName}
|
||||
/>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Cache trigger meters"
|
||||
keyboardType="numeric"
|
||||
value={cacheTriggerMeters}
|
||||
onChangeText={setCacheTriggerMeters}
|
||||
/>
|
||||
<Pressable style={styles.button} onPress={addSubgroup}>
|
||||
<Text style={styles.buttonText}>Create Subgroup</Text>
|
||||
</Pressable>
|
||||
{!!error && <Text style={styles.errorText}>{error}</Text>}
|
||||
</View>
|
||||
<AdminFooterNavbar />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
wrapper: {
|
||||
flex: 1,
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
gap: 10,
|
||||
padding: 20,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: '700',
|
||||
color: '#111827',
|
||||
},
|
||||
row: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#d1d5db',
|
||||
borderRadius: 8,
|
||||
padding: 10,
|
||||
gap: 8,
|
||||
},
|
||||
rowText: {
|
||||
color: '#111827',
|
||||
fontSize: 13,
|
||||
},
|
||||
inlineButton: {
|
||||
alignSelf: 'flex-start',
|
||||
backgroundColor: '#2563eb',
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 6,
|
||||
},
|
||||
inlineButtonText: {
|
||||
color: '#ffffff',
|
||||
fontWeight: '600',
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#d1d5db',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
button: {
|
||||
backgroundColor: '#2563eb',
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
paddingVertical: 12,
|
||||
},
|
||||
buttonText: {
|
||||
color: '#ffffff',
|
||||
fontWeight: '600',
|
||||
},
|
||||
errorText: {
|
||||
color: '#dc2626',
|
||||
},
|
||||
});
|
||||
|
||||
export default ManageSubgroupsPage;
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { View, Text, StyleSheet } from 'react-native';
|
||||
import MemberFooterNavbar from '../components/MemberFooterNavbar';
|
||||
import useMapViewModel from '../ViewModel/useMapViewModel';
|
||||
|
||||
const MapPage = () => {
|
||||
const { points } = useMapViewModel();
|
||||
|
||||
return (
|
||||
<View style={styles.wrapper}>
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Map Page</Text>
|
||||
<Text style={styles.body}>Map and hunt markers appear here.</Text>
|
||||
<Text style={styles.body}>Points loaded: {points.length}</Text>
|
||||
</View>
|
||||
<MemberFooterNavbar visible />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
wrapper: {
|
||||
flex: 1,
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 20,
|
||||
gap: 6,
|
||||
},
|
||||
title: {
|
||||
fontSize: 26,
|
||||
fontWeight: '700',
|
||||
color: '#111827',
|
||||
},
|
||||
body: {
|
||||
color: '#4b5563',
|
||||
},
|
||||
});
|
||||
|
||||
export default MapPage;
|
||||
|
||||
+132
-19
@@ -1,31 +1,144 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, TextInput, Button, Picker } from 'react-native';
|
||||
import { View, Text, TextInput, Pressable, StyleSheet } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import useAuthViewModel from '../ViewModel/useAuthViewModel';
|
||||
|
||||
const RegisterPage = () => {
|
||||
const [username, setUsername] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [userType, setUserType] = useState('Individual');
|
||||
const navigation = useNavigation();
|
||||
const {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
confirmPassword,
|
||||
accountType,
|
||||
error,
|
||||
setUsername,
|
||||
setEmail,
|
||||
setPassword,
|
||||
setConfirmPassword,
|
||||
setAccountType,
|
||||
register,
|
||||
} = useAuthViewModel();
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const handleRegister = () => {
|
||||
// Registration logic here
|
||||
const handleRegister = async () => {
|
||||
setSubmitting(true);
|
||||
const user = await register();
|
||||
setSubmitting(false);
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
navigation.navigate(user.SGid === 0 ? 'admin-hub' : 'game-type');
|
||||
};
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Text>Register</Text>
|
||||
<TextInput placeholder="Username" value={username} onChangeText={setUsername} />
|
||||
<TextInput placeholder="Email" value={email} onChangeText={setEmail} />
|
||||
<TextInput placeholder="Password" value={password} onChangeText={setPassword} secureTextEntry />
|
||||
<TextInput placeholder="Confirm Password" value={confirmPassword} onChangeText={setConfirmPassword} secureTextEntry />
|
||||
<Picker selectedValue={userType} onValueChange={(itemValue) => setUserType(itemValue)}>
|
||||
<Picker.Item label="Individual" value="Individual" />
|
||||
<Picker.Item label="Business/School" value="Business/School" />
|
||||
</Picker>
|
||||
<Button title="Register" onPress={handleRegister} />
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Register</Text>
|
||||
<TextInput style={styles.input} placeholder="Username" value={username} onChangeText={setUsername} />
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Email"
|
||||
autoCapitalize="none"
|
||||
keyboardType="email-address"
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
/>
|
||||
<TextInput style={styles.input} placeholder="Password" value={password} onChangeText={setPassword} secureTextEntry />
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Confirm Password"
|
||||
value={confirmPassword}
|
||||
onChangeText={setConfirmPassword}
|
||||
secureTextEntry
|
||||
/>
|
||||
<View style={styles.typeRow}>
|
||||
<Pressable
|
||||
style={[styles.typeButton, accountType === 'Individual' && styles.typeButtonActive]}
|
||||
onPress={() => setAccountType('Individual')}
|
||||
>
|
||||
<Text style={[styles.typeText, accountType === 'Individual' && styles.typeTextActive]}>Individual</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={[styles.typeButton, accountType === 'Business/School' && styles.typeButtonActive]}
|
||||
onPress={() => setAccountType('Business/School')}
|
||||
>
|
||||
<Text style={[styles.typeText, accountType === 'Business/School' && styles.typeTextActive]}>Business/School</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
<Pressable style={styles.submitButton} onPress={handleRegister}>
|
||||
<Text style={styles.submitText}>{submitting ? 'Creating...' : 'Create Account'}</Text>
|
||||
</Pressable>
|
||||
<Pressable onPress={() => navigation.navigate('login')}>
|
||||
<Text style={styles.backText}>Back to Login</Text>
|
||||
</Pressable>
|
||||
{!!error && <Text style={styles.errorText}>{error}</Text>}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 20,
|
||||
justifyContent: 'center',
|
||||
gap: 12,
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
title: {
|
||||
fontSize: 28,
|
||||
fontWeight: '700',
|
||||
color: '#111827',
|
||||
marginBottom: 8,
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#d1d5db',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
typeRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 10,
|
||||
},
|
||||
typeButton: {
|
||||
flex: 1,
|
||||
borderWidth: 1,
|
||||
borderColor: '#cbd5e1',
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
paddingVertical: 10,
|
||||
},
|
||||
typeButtonActive: {
|
||||
backgroundColor: '#2563eb',
|
||||
borderColor: '#2563eb',
|
||||
},
|
||||
typeText: {
|
||||
color: '#1f2937',
|
||||
fontWeight: '600',
|
||||
},
|
||||
typeTextActive: {
|
||||
color: '#ffffff',
|
||||
},
|
||||
submitButton: {
|
||||
backgroundColor: '#2563eb',
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
paddingVertical: 12,
|
||||
},
|
||||
submitText: {
|
||||
color: '#ffffff',
|
||||
fontWeight: '600',
|
||||
},
|
||||
backText: {
|
||||
textAlign: 'center',
|
||||
color: '#2563eb',
|
||||
fontWeight: '600',
|
||||
},
|
||||
errorText: {
|
||||
textAlign: 'center',
|
||||
color: '#dc2626',
|
||||
},
|
||||
});
|
||||
|
||||
export default RegisterPage;
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { View, Text, StyleSheet } from 'react-native';
|
||||
import MemberFooterNavbar from '../components/MemberFooterNavbar';
|
||||
import useTeamViewModel from '../ViewModel/useTeamViewModel';
|
||||
|
||||
const TeamPage = () => {
|
||||
const { team } = useTeamViewModel();
|
||||
|
||||
return (
|
||||
<View style={styles.wrapper}>
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Team Page</Text>
|
||||
<Text style={styles.body}>Team details and members appear here.</Text>
|
||||
<Text style={styles.body}>{team?.team?.TeamName || 'No team joined yet'}</Text>
|
||||
</View>
|
||||
<MemberFooterNavbar visible />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
wrapper: {
|
||||
flex: 1,
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 20,
|
||||
gap: 6,
|
||||
},
|
||||
title: {
|
||||
fontSize: 26,
|
||||
fontWeight: '700',
|
||||
color: '#111827',
|
||||
},
|
||||
body: {
|
||||
color: '#4b5563',
|
||||
},
|
||||
});
|
||||
|
||||
export default TeamPage;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
approveAdminAccess,
|
||||
createPrivateGame,
|
||||
requestAdminAccess,
|
||||
updateGroupSettings,
|
||||
} from '../API/API';
|
||||
import { getSession, setSessionGroup } from '../Model/SessionStore';
|
||||
|
||||
const useAdminViewModel = () => {
|
||||
const createGame = async (payload) => {
|
||||
const { currentUid } = getSession();
|
||||
const group = await createPrivateGame({ ...payload, userId: currentUid });
|
||||
setSessionGroup(group.Gid, 0);
|
||||
return group;
|
||||
};
|
||||
const requestAdmin = async (payload) => requestAdminAccess(payload);
|
||||
const approveAdmin = async (payload) => approveAdminAccess(payload);
|
||||
const setTeamsEnabled = async (gid, teamsEnabled) => updateGroupSettings({ gid, teamsEnabled });
|
||||
|
||||
return {
|
||||
createGame,
|
||||
requestAdmin,
|
||||
approveAdmin,
|
||||
setTeamsEnabled,
|
||||
};
|
||||
};
|
||||
|
||||
export default useAdminViewModel;
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { loginUser, registerUser } from '../API/API';
|
||||
import { setSessionUser } from '../Model/SessionStore';
|
||||
|
||||
const useAuthViewModel = () => {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [username, setUsername] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [accountType, setAccountType] = useState('Individual');
|
||||
const [error, setError] = useState('');
|
||||
const [currentUser, setCurrentUser] = useState(null);
|
||||
|
||||
const canRegister = useMemo(
|
||||
() => Boolean(username && email && password && confirmPassword),
|
||||
[username, email, password, confirmPassword],
|
||||
);
|
||||
|
||||
const login = async () => {
|
||||
setError('');
|
||||
try {
|
||||
const user = await loginUser(email.trim(), password);
|
||||
setCurrentUser(user);
|
||||
setSessionUser(user);
|
||||
return user;
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const register = async () => {
|
||||
setError('');
|
||||
try {
|
||||
const user = await registerUser({
|
||||
username: username.trim(),
|
||||
email: email.trim().toLowerCase(),
|
||||
password,
|
||||
confirmPassword,
|
||||
accountType,
|
||||
});
|
||||
setCurrentUser(user);
|
||||
setSessionUser(user);
|
||||
return user;
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
email,
|
||||
password,
|
||||
username,
|
||||
confirmPassword,
|
||||
accountType,
|
||||
error,
|
||||
currentUser,
|
||||
canRegister,
|
||||
setEmail,
|
||||
setPassword,
|
||||
setUsername,
|
||||
setConfirmPassword,
|
||||
setAccountType,
|
||||
login,
|
||||
register,
|
||||
};
|
||||
};
|
||||
|
||||
export default useAuthViewModel;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getGameTypes } from '../API/API';
|
||||
|
||||
const useGameTypeViewModel = () => {
|
||||
const [gameTypes, setGameTypes] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
getGameTypes().then((types) => {
|
||||
if (mounted) {
|
||||
setGameTypes(types);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { gameTypes };
|
||||
};
|
||||
|
||||
export default useGameTypeViewModel;
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useState } from 'react';
|
||||
import { createTeam, getLobby, joinTeam } from '../API/API';
|
||||
import { getSession, setSessionTeam } from '../Model/SessionStore';
|
||||
|
||||
const useLobbyViewModel = () => {
|
||||
const [teamCode, setTeamCode] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const loadLobby = async () => {
|
||||
const { currentGid } = getSession();
|
||||
return getLobby(currentGid);
|
||||
};
|
||||
|
||||
const createLobbyTeam = async ({ teamName }) => {
|
||||
setError('');
|
||||
try {
|
||||
const { currentUid, currentGid } = getSession();
|
||||
const team = await createTeam({ userId: currentUid, gid: currentGid, teamName });
|
||||
setSessionTeam(team.Tid);
|
||||
return team;
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const joinLobbyTeam = async () => {
|
||||
setError('');
|
||||
try {
|
||||
const { currentUid } = getSession();
|
||||
const team = await joinTeam({ userId: currentUid, teamCode: teamCode.trim().toUpperCase() });
|
||||
setSessionTeam(team.Tid);
|
||||
return team;
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
teamCode,
|
||||
error,
|
||||
setTeamCode,
|
||||
loadLobby,
|
||||
createLobbyTeam,
|
||||
joinLobbyTeam,
|
||||
};
|
||||
};
|
||||
|
||||
export default useLobbyViewModel;
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { useState } from 'react';
|
||||
import { createPrivateGame, joinAsAdmin } from '../API/API';
|
||||
import { getSession, setSessionGroup } from '../Model/SessionStore';
|
||||
|
||||
const useManagePrivateSubtaskViewModel = () => {
|
||||
const [businessOrSchoolName, setBusinessOrSchoolName] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const createNewPrivateGame = async () => {
|
||||
setError('');
|
||||
const { currentUid } = getSession();
|
||||
try {
|
||||
const group = await createPrivateGame({
|
||||
userId: currentUid,
|
||||
groupName: businessOrSchoolName.trim() || 'New Private Game',
|
||||
businessOrSchoolName: businessOrSchoolName.trim() || 'Unnamed Business',
|
||||
isBusiness: true,
|
||||
});
|
||||
setSessionGroup(group.Gid, 0);
|
||||
return group;
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const joinAdminForPrivate = async () => {
|
||||
setError('');
|
||||
const { currentUid } = getSession();
|
||||
try {
|
||||
const response = await joinAsAdmin({
|
||||
userId: currentUid,
|
||||
businessOrSchoolName: businessOrSchoolName.trim(),
|
||||
});
|
||||
const gid = response?.Gid ?? null;
|
||||
if (gid !== null) {
|
||||
setSessionGroup(gid, 0);
|
||||
}
|
||||
return response;
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
businessOrSchoolName,
|
||||
error,
|
||||
setBusinessOrSchoolName,
|
||||
createNewPrivateGame,
|
||||
joinAdminForPrivate,
|
||||
};
|
||||
};
|
||||
|
||||
export default useManagePrivateSubtaskViewModel;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getMapPoints } from '../API/API';
|
||||
import { getSession } from '../Model/SessionStore';
|
||||
|
||||
const useMapViewModel = (groupId = null) => {
|
||||
const [points, setPoints] = useState([]);
|
||||
const { currentGid, currentSGid } = getSession();
|
||||
const resolvedGid = groupId ?? currentGid;
|
||||
|
||||
useEffect(() => {
|
||||
if (!resolvedGid) {
|
||||
setPoints([]);
|
||||
return;
|
||||
}
|
||||
let active = true;
|
||||
getMapPoints(resolvedGid, currentSGid).then((payload) => {
|
||||
if (active) {
|
||||
setPoints(payload);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [resolvedGid, currentSGid]);
|
||||
|
||||
return { points };
|
||||
};
|
||||
|
||||
export default useMapViewModel;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useState } from 'react';
|
||||
import { joinPrivateGame } from '../API/API';
|
||||
import { getSession, setSessionGroup } from '../Model/SessionStore';
|
||||
|
||||
const usePrivateGameViewModel = () => {
|
||||
const [joinCode, setJoinCode] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const joinWithCode = async () => {
|
||||
setError('');
|
||||
try {
|
||||
const { currentUid } = getSession();
|
||||
const group = await joinPrivateGame({ userId: currentUid, joinCode: joinCode.trim().toUpperCase() });
|
||||
setSessionGroup(group.Gid);
|
||||
return group;
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
joinCode,
|
||||
error,
|
||||
setJoinCode,
|
||||
joinWithCode,
|
||||
};
|
||||
};
|
||||
|
||||
export default usePrivateGameViewModel;
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { createSubgroup, getSubgroups, updateSubgroupCache } from '../API/API';
|
||||
import { getSession } from '../Model/SessionStore';
|
||||
|
||||
const useSubgroupAdminViewModel = () => {
|
||||
const [subgroups, setSubgroups] = useState([]);
|
||||
const [subgroupName, setSubgroupName] = useState('');
|
||||
const [cacheTriggerMeters, setCacheTriggerMeters] = useState('20');
|
||||
const [error, setError] = useState('');
|
||||
const { currentGid } = getSession();
|
||||
|
||||
const loadSubgroups = useCallback(async () => {
|
||||
if (!currentGid) {
|
||||
return;
|
||||
}
|
||||
const rows = await getSubgroups(currentGid);
|
||||
setSubgroups(rows);
|
||||
}, [currentGid]);
|
||||
|
||||
useEffect(() => {
|
||||
loadSubgroups();
|
||||
}, [loadSubgroups]);
|
||||
|
||||
const addSubgroup = async () => {
|
||||
setError('');
|
||||
try {
|
||||
await createSubgroup({
|
||||
gid: currentGid,
|
||||
subgroupName: subgroupName.trim(),
|
||||
cacheTriggerMeters: Number(cacheTriggerMeters),
|
||||
});
|
||||
setSubgroupName('');
|
||||
await loadSubgroups();
|
||||
return true;
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const updateCacheForSubgroup = async (sgid, triggerMeters) => {
|
||||
setError('');
|
||||
try {
|
||||
await updateSubgroupCache({ gid: currentGid, sgid, triggerMeters });
|
||||
await loadSubgroups();
|
||||
return true;
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
subgroups,
|
||||
subgroupName,
|
||||
cacheTriggerMeters,
|
||||
error,
|
||||
setSubgroupName,
|
||||
setCacheTriggerMeters,
|
||||
addSubgroup,
|
||||
updateCacheForSubgroup,
|
||||
};
|
||||
};
|
||||
|
||||
export default useSubgroupAdminViewModel;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getTeam } from '../API/API';
|
||||
import { getSession } from '../Model/SessionStore';
|
||||
|
||||
const useTeamViewModel = (teamId = null) => {
|
||||
const [team, setTeam] = useState(null);
|
||||
const resolvedTeamId = teamId ?? getSession().currentTGid;
|
||||
|
||||
useEffect(() => {
|
||||
if (!resolvedTeamId) {
|
||||
setTeam(null);
|
||||
return;
|
||||
}
|
||||
let active = true;
|
||||
getTeam(resolvedTeamId).then((payload) => {
|
||||
if (active) {
|
||||
setTeam(payload);
|
||||
}
|
||||
}).catch(() => {
|
||||
if (active) {
|
||||
setTeam(null);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [resolvedTeamId]);
|
||||
|
||||
return { team };
|
||||
};
|
||||
|
||||
export default useTeamViewModel;
|
||||
|
||||
+15
-1
@@ -1,5 +1,19 @@
|
||||
import { Stack } from "expo-router";
|
||||
|
||||
export default function RootLayout() {
|
||||
return <Stack />;
|
||||
return (
|
||||
<Stack initialRouteName="index">
|
||||
<Stack.Screen name="index" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="login" options={{ title: "Login" }} />
|
||||
<Stack.Screen name="register" options={{ title: "Register" }} />
|
||||
<Stack.Screen name="game-type" options={{ title: "Select Game Type" }} />
|
||||
<Stack.Screen name="join-private-game" options={{ title: "Join Private Game" }} />
|
||||
<Stack.Screen name="manage-private-subtask" options={{ title: "Manage Private Game" }} />
|
||||
<Stack.Screen name="game-lobby" options={{ title: "Game Lobby" }} />
|
||||
<Stack.Screen name="team" options={{ title: "Team" }} />
|
||||
<Stack.Screen name="map" options={{ title: "Map" }} />
|
||||
<Stack.Screen name="admin-hub" options={{ title: "Admin Hub" }} />
|
||||
<Stack.Screen name="manage-subgroups" options={{ title: "Manage Subgroups" }} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import AdminHubPage from './Screens/AdminHubPage';
|
||||
|
||||
export default AdminHubPage;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import { View, Pressable, Text, StyleSheet } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
||||
const AdminFooterNavbar = () => {
|
||||
const navigation = useNavigation();
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Pressable style={styles.button} onPress={() => navigation.navigate('admin-hub')}>
|
||||
<Text style={styles.text}>Admin Hub</Text>
|
||||
</Pressable>
|
||||
<Pressable style={styles.button} onPress={() => navigation.navigate('game-lobby')}>
|
||||
<Text style={styles.text}>Manage Game</Text>
|
||||
</Pressable>
|
||||
<Pressable style={styles.button} onPress={() => navigation.navigate('team')}>
|
||||
<Text style={styles.text}>Manage Team</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
borderTopWidth: 1,
|
||||
borderColor: '#d4d4d8',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 10,
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
button: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
},
|
||||
text: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#111827',
|
||||
},
|
||||
});
|
||||
|
||||
export default AdminFooterNavbar;
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import { View, Pressable, Text, StyleSheet } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
||||
const MemberFooterNavbar = ({ visible = false }) => {
|
||||
const navigation = useNavigation();
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Pressable style={styles.button} onPress={() => navigation.navigate('game-lobby')}>
|
||||
<Text style={styles.text}>Lobby</Text>
|
||||
</Pressable>
|
||||
<Pressable style={styles.button} onPress={() => navigation.navigate('team')}>
|
||||
<Text style={styles.text}>Team</Text>
|
||||
</Pressable>
|
||||
<Pressable style={styles.button} onPress={() => navigation.navigate('map')}>
|
||||
<Text style={styles.text}>Map</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
borderTopWidth: 1,
|
||||
borderColor: '#d4d4d8',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 10,
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
button: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
},
|
||||
text: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#111827',
|
||||
},
|
||||
});
|
||||
|
||||
export default MemberFooterNavbar;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import GameLobbyPage from './Screens/GameLobbyPage';
|
||||
|
||||
export default GameLobbyPage;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import GameTypePage from './Screens/GameTypePage';
|
||||
|
||||
export default GameTypePage;
|
||||
|
||||
+2
-12
@@ -1,15 +1,5 @@
|
||||
import { Text, View } from "react-native";
|
||||
import { Redirect } from "expo-router";
|
||||
|
||||
export default function Index() {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Text>Cat testing</Text>
|
||||
</View>
|
||||
);
|
||||
return <Redirect href="/login" />;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import JoinPrivateGamePage from './Screens/JoinPrivateGamePage';
|
||||
|
||||
export default JoinPrivateGamePage;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import LoginPage from './Screens/LoginPage';
|
||||
|
||||
export default LoginPage;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import ManagePrivateSubtaskPage from './Screens/ManagePrivateSubtaskPage';
|
||||
|
||||
export default ManagePrivateSubtaskPage;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import ManageSubgroupsPage from './Screens/ManageSubgroupsPage';
|
||||
|
||||
export default ManageSubgroupsPage;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import MapPage from './Screens/MapPage';
|
||||
|
||||
export default MapPage;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import RegisterPage from './Screens/RegisterPage';
|
||||
|
||||
export default RegisterPage;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import TeamPage from './Screens/TeamPage';
|
||||
|
||||
export default TeamPage;
|
||||
|
||||
Reference in new issue
Block a user