mirror of
https://github.com/mudabbir-ahmad/MAD-Treasure-Hunt.git
synced 2026-10-08 03:00:20 +00:00
Merge pull request #7 from mudabbir-ahmad/global-game
Global game logic
This commit is contained in:
20 files changed
+1752
-26
No files matched your search
@@ -1,5 +1,9 @@
|
|||||||
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
|
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
|
||||||
|
|
||||||
|
# Environment variables (contains secrets)
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ import GameSettingsScreen from "./src/components/screens/GameSettingsScreen";
|
|||||||
import DepartmentSettingsScreen from "./src/components/screens/DepartmentSettingsScreen";
|
import DepartmentSettingsScreen from "./src/components/screens/DepartmentSettingsScreen";
|
||||||
import PlayersScreen from "./src/components/screens/PlayersScreen";
|
import PlayersScreen from "./src/components/screens/PlayersScreen";
|
||||||
import ExpandedMapScreen from "./src/components/screens/ExpandedMapScreen";
|
import ExpandedMapScreen from "./src/components/screens/ExpandedMapScreen";
|
||||||
|
import GameSelectionScreen from "./src/components/screens/GameSelectionScreen";
|
||||||
|
import GlobalEventsScreen from "./src/components/screens/global/GlobalEventsScreen";
|
||||||
|
import GlobalMapScreen from "./src/components/screens/global/GlobalMapScreen";
|
||||||
|
import GlobalCacheViewScreen from "./src/components/screens/global/GlobalCacheViewScreen";
|
||||||
|
import GlobalLeaderboardScreen from "./src/components/screens/global/GlobalLeaderboardScreen";
|
||||||
import { loadSession } from "./src/hooks/SessionStore";
|
import { loadSession } from "./src/hooks/SessionStore";
|
||||||
|
|
||||||
const RootStack = createNativeStackNavigator();
|
const RootStack = createNativeStackNavigator();
|
||||||
@@ -21,11 +26,13 @@ const AdminStackNav = createNativeStackNavigator();
|
|||||||
|
|
||||||
const screenOptions = {
|
const screenOptions = {
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
contentStyle: {backgroundColor: '#ffffff'},
|
contentStyle: { backgroundColor: "#ffffff" },
|
||||||
};
|
};
|
||||||
|
|
||||||
const AuthStack = () => (
|
const AuthStack = () => (
|
||||||
<AuthStackNav.Navigator screenOptions={{...screenOptions, keyboardHandlingEnabled: false}}>
|
<AuthStackNav.Navigator
|
||||||
|
screenOptions={{ ...screenOptions, keyboardHandlingEnabled: false }}
|
||||||
|
>
|
||||||
<AuthStackNav.Screen name="LoginScreen" component={LoginScreen} />
|
<AuthStackNav.Screen name="LoginScreen" component={LoginScreen} />
|
||||||
<AuthStackNav.Screen name="RegisterScreen" component={RegisterScreen} />
|
<AuthStackNav.Screen name="RegisterScreen" component={RegisterScreen} />
|
||||||
</AuthStackNav.Navigator>
|
</AuthStackNav.Navigator>
|
||||||
@@ -33,15 +40,72 @@ const AuthStack = () => (
|
|||||||
|
|
||||||
const GameStack = () => (
|
const GameStack = () => (
|
||||||
<GameStackNav.Navigator screenOptions={screenOptions}>
|
<GameStackNav.Navigator screenOptions={screenOptions}>
|
||||||
|
<GameStackNav.Screen
|
||||||
|
name="GameSelectionScreen"
|
||||||
|
component={GameSelectionScreen}
|
||||||
|
/>
|
||||||
<GameStackNav.Screen name="MapScreen" component={MapScreen} />
|
<GameStackNav.Screen name="MapScreen" component={MapScreen} />
|
||||||
<GameStackNav.Screen name="TeamScreen" component={TeamScreen} />
|
<GameStackNav.Screen name="TeamScreen" component={TeamScreen} />
|
||||||
<GameStackNav.Screen name="LeaderboardScreen" component={LeaderboardScreen}/>
|
<GameStackNav.Screen
|
||||||
|
name="LeaderboardScreen"
|
||||||
|
component={LeaderboardScreen}
|
||||||
|
/>
|
||||||
<GameStackNav.Screen name="PlayersScreen" component={PlayersScreen} />
|
<GameStackNav.Screen name="PlayersScreen" component={PlayersScreen} />
|
||||||
<GameStackNav.Screen name="ExpandedMapScreen" component={ExpandedMapScreen}/>
|
<GameStackNav.Screen
|
||||||
|
name="ExpandedMapScreen"
|
||||||
|
component={ExpandedMapScreen}
|
||||||
|
/>
|
||||||
|
<GameStackNav.Screen
|
||||||
|
name="GlobalEventsScreen"
|
||||||
|
component={GlobalEventsScreen}
|
||||||
|
/>
|
||||||
|
<GameStackNav.Screen name="GlobalMapScreen" component={GlobalMapScreen} />
|
||||||
|
<GameStackNav.Screen
|
||||||
|
name="GlobalCacheViewScreen"
|
||||||
|
component={GlobalCacheViewScreen}
|
||||||
|
/>
|
||||||
|
<GameStackNav.Screen
|
||||||
|
name="GlobalLeaderboardScreen"
|
||||||
|
component={GlobalLeaderboardScreen}
|
||||||
|
/>
|
||||||
</GameStackNav.Navigator>
|
</GameStackNav.Navigator>
|
||||||
);
|
);
|
||||||
|
|
||||||
const AdminStack = () => (
|
const AdminStack = () => (
|
||||||
|
<AdminStackNav.Navigator screenOptions={screenOptions}>
|
||||||
|
<AdminStackNav.Screen
|
||||||
|
name="GameSelectionScreen"
|
||||||
|
component={GameSelectionScreen}
|
||||||
|
/>
|
||||||
|
<AdminStackNav.Screen
|
||||||
|
name="GameSettingsScreen"
|
||||||
|
component={GameSettingsScreen}
|
||||||
|
/>
|
||||||
|
<AdminStackNav.Screen name="MapScreen" component={MapScreen} />
|
||||||
|
<AdminStackNav.Screen
|
||||||
|
name="LeaderboardScreen"
|
||||||
|
component={LeaderboardScreen}
|
||||||
|
/>
|
||||||
|
<AdminStackNav.Screen name="PlayersScreen" component={PlayersScreen} />
|
||||||
|
<AdminStackNav.Screen
|
||||||
|
name="ExpandedMapScreen"
|
||||||
|
component={ExpandedMapScreen}
|
||||||
|
/>
|
||||||
|
<AdminStackNav.Screen name="TeamScreen" component={TeamScreen} />
|
||||||
|
<AdminStackNav.Screen
|
||||||
|
name="GlobalEventsScreen"
|
||||||
|
component={GlobalEventsScreen}
|
||||||
|
/>
|
||||||
|
<AdminStackNav.Screen name="GlobalMapScreen" component={GlobalMapScreen} />
|
||||||
|
<AdminStackNav.Screen
|
||||||
|
name="GlobalCacheViewScreen"
|
||||||
|
component={GlobalCacheViewScreen}
|
||||||
|
/>
|
||||||
|
<AdminStackNav.Screen
|
||||||
|
name="GlobalLeaderboardScreen"
|
||||||
|
component={GlobalLeaderboardScreen}
|
||||||
|
/>
|
||||||
|
</AdminStackNav.Navigator>
|
||||||
<AdminStackNav.Navigator screenOptions={screenOptions}>
|
<AdminStackNav.Navigator screenOptions={screenOptions}>
|
||||||
<AdminStackNav.Screen name="GameSettingsScreen" component={GameSettingsScreen}/>
|
<AdminStackNav.Screen name="GameSettingsScreen" component={GameSettingsScreen}/>
|
||||||
<AdminStackNav.Screen name="DepartmentSettingsScreen" component={DepartmentSettingsScreen}/>
|
<AdminStackNav.Screen name="DepartmentSettingsScreen" component={DepartmentSettingsScreen}/>
|
||||||
@@ -56,12 +120,12 @@ const AdminStack = () => (
|
|||||||
export const App = () => {
|
export const App = () => {
|
||||||
// Load persisted session from AsyncStorage before choosing initial route
|
// Load persisted session from AsyncStorage before choosing initial route
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [initialRoute, setInitialRoute] = useState('Auth');
|
const [initialRoute, setInitialRoute] = useState("Auth");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadSession().then((session) => {
|
loadSession().then((session) => {
|
||||||
if (session.currentUid) {
|
if (session.currentUid) {
|
||||||
setInitialRoute(session.isAcceptedAdmin ? 'Admin' : 'Game');
|
setInitialRoute(session.isAcceptedAdmin ? "Admin" : "Game");
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
@@ -69,7 +133,14 @@ export const App = () => {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<View style={{flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#fff'}}>
|
<View
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<ActivityIndicator size="large" />
|
<ActivityIndicator size="large" />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -78,7 +149,10 @@ export const App = () => {
|
|||||||
return (
|
return (
|
||||||
<SafeAreaProvider>
|
<SafeAreaProvider>
|
||||||
<NavigationContainer>
|
<NavigationContainer>
|
||||||
<RootStack.Navigator initialRouteName={initialRoute} screenOptions={screenOptions}>
|
<RootStack.Navigator
|
||||||
|
initialRouteName={initialRoute}
|
||||||
|
screenOptions={screenOptions}
|
||||||
|
>
|
||||||
<RootStack.Screen name="Auth" component={AuthStack} />
|
<RootStack.Screen name="Auth" component={AuthStack} />
|
||||||
<RootStack.Screen name="Game" component={GameStack} />
|
<RootStack.Screen name="Game" component={GameStack} />
|
||||||
<RootStack.Screen name="Admin" component={AdminStack} />
|
<RootStack.Screen name="Admin" component={AdminStack} />
|
||||||
|
|||||||
Generated
+19
@@ -77,6 +77,7 @@
|
|||||||
"version": "7.29.0",
|
"version": "7.29.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
|
||||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.0",
|
"@babel/code-frame": "^7.29.0",
|
||||||
"@babel/generator": "^7.29.0",
|
"@babel/generator": "^7.29.0",
|
||||||
@@ -1364,6 +1365,7 @@
|
|||||||
"version": "7.28.6",
|
"version": "7.28.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
|
||||||
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
|
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
@@ -2627,6 +2629,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.2.2.tgz",
|
||||||
"integrity": "sha512-kem1Ko2BcbAjmbQIv66dNmr6EtfDut3QU0qjsVhMnLLhktwyXb6FzZYp8gTrUb6AvkAbaJoi+BF5Pl55pAUa5w==",
|
"integrity": "sha512-kem1Ko2BcbAjmbQIv66dNmr6EtfDut3QU0qjsVhMnLLhktwyXb6FzZYp8gTrUb6AvkAbaJoi+BF5Pl55pAUa5w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-navigation/core": "^7.17.2",
|
"@react-navigation/core": "^7.17.2",
|
||||||
"escape-string-regexp": "^4.0.0",
|
"escape-string-regexp": "^4.0.0",
|
||||||
@@ -2867,6 +2870,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz",
|
||||||
"integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==",
|
"integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.57.0",
|
"@typescript-eslint/scope-manager": "8.57.0",
|
||||||
"@typescript-eslint/types": "8.57.0",
|
"@typescript-eslint/types": "8.57.0",
|
||||||
@@ -3415,6 +3419,7 @@
|
|||||||
"version": "8.16.0",
|
"version": "8.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -4043,6 +4048,7 @@
|
|||||||
"url": "https://github.com/sponsors/ai"
|
"url": "https://github.com/sponsors/ai"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@@ -4927,6 +4933,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz",
|
||||||
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
|
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -5114,6 +5121,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
|
||||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rtsao/scc": "^1.1.0",
|
"@rtsao/scc": "^1.1.0",
|
||||||
"array-includes": "^3.1.9",
|
"array-includes": "^3.1.9",
|
||||||
@@ -5337,6 +5345,7 @@
|
|||||||
"version": "54.0.33",
|
"version": "54.0.33",
|
||||||
"resolved": "https://registry.npmjs.org/expo/-/expo-54.0.33.tgz",
|
"resolved": "https://registry.npmjs.org/expo/-/expo-54.0.33.tgz",
|
||||||
"integrity": "sha512-3yOEfAKqo+gqHcV8vKcnq0uA5zxlohnhA3fu4G43likN8ct5ZZ3LjAh9wDdKteEkoad3tFPvwxmXW711S5OHUw==",
|
"integrity": "sha512-3yOEfAKqo+gqHcV8vKcnq0uA5zxlohnhA3fu4G43likN8ct5ZZ3LjAh9wDdKteEkoad3tFPvwxmXW711S5OHUw==",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.0",
|
"@babel/runtime": "^7.20.0",
|
||||||
"@expo/cli": "54.0.23",
|
"@expo/cli": "54.0.23",
|
||||||
@@ -5424,6 +5433,7 @@
|
|||||||
"version": "14.0.11",
|
"version": "14.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.11.tgz",
|
||||||
"integrity": "sha512-ga0q61ny4s/kr4k8JX9hVH69exVSIfcIc19+qZ7gt71Mqtm7xy2c6kwsPTCyhBW2Ro5yXTT8EaZOpuRi35rHbg==",
|
"integrity": "sha512-ga0q61ny4s/kr4k8JX9hVH69exVSIfcIc19+qZ7gt71Mqtm7xy2c6kwsPTCyhBW2Ro5yXTT8EaZOpuRi35rHbg==",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fontfaceobserver": "^2.1.0"
|
"fontfaceobserver": "^2.1.0"
|
||||||
},
|
},
|
||||||
@@ -8941,6 +8951,7 @@
|
|||||||
"version": "19.1.0",
|
"version": "19.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -8958,6 +8969,7 @@
|
|||||||
"version": "19.1.0",
|
"version": "19.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.26.0"
|
"scheduler": "^0.26.0"
|
||||||
},
|
},
|
||||||
@@ -8986,6 +8998,7 @@
|
|||||||
"version": "0.81.5",
|
"version": "0.81.5",
|
||||||
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.5.tgz",
|
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.5.tgz",
|
||||||
"integrity": "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw==",
|
"integrity": "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw==",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jest/create-cache-key-function": "^29.7.0",
|
"@jest/create-cache-key-function": "^29.7.0",
|
||||||
"@react-native/assets-registry": "0.81.5",
|
"@react-native/assets-registry": "0.81.5",
|
||||||
@@ -9113,6 +9126,7 @@
|
|||||||
"version": "5.6.2",
|
"version": "5.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz",
|
||||||
"integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==",
|
"integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==",
|
||||||
|
"peer": true,
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "*",
|
"react": "*",
|
||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
@@ -9122,6 +9136,7 @@
|
|||||||
"version": "4.16.0",
|
"version": "4.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.16.0.tgz",
|
||||||
"integrity": "sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==",
|
"integrity": "sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react-freeze": "^1.0.0",
|
"react-freeze": "^1.0.0",
|
||||||
"react-native-is-edge-to-edge": "^1.2.1",
|
"react-native-is-edge-to-edge": "^1.2.1",
|
||||||
@@ -9136,6 +9151,7 @@
|
|||||||
"version": "0.21.2",
|
"version": "0.21.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz",
|
||||||
"integrity": "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==",
|
"integrity": "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.18.6",
|
"@babel/runtime": "^7.18.6",
|
||||||
"@react-native/normalize-colors": "^0.74.1",
|
"@react-native/normalize-colors": "^0.74.1",
|
||||||
@@ -9165,6 +9181,7 @@
|
|||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.5.1.tgz",
|
||||||
"integrity": "sha512-lJG6Uk9YuojjEX/tQrCbcbmpdLCSFxDK1rJlkDhgqkVi1KZzG7cdcBFQRqyNOOzR9Y0CXNuldmtWTGOyM0k0+w==",
|
"integrity": "sha512-lJG6Uk9YuojjEX/tQrCbcbmpdLCSFxDK1rJlkDhgqkVi1KZzG7cdcBFQRqyNOOzR9Y0CXNuldmtWTGOyM0k0+w==",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/plugin-transform-arrow-functions": "^7.0.0-0",
|
"@babel/plugin-transform-arrow-functions": "^7.0.0-0",
|
||||||
"@babel/plugin-transform-class-properties": "^7.0.0-0",
|
"@babel/plugin-transform-class-properties": "^7.0.0-0",
|
||||||
@@ -9268,6 +9285,7 @@
|
|||||||
"version": "0.14.2",
|
"version": "0.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
|
||||||
"integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
|
"integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -10385,6 +10403,7 @@
|
|||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
//export const API_BASE_URL = 'http://api.bobby.ip-ddns.com'; // For actual use
|
export const API_BASE_URL = "http://api.bobby.ip-ddns.com"; // For actual use
|
||||||
export const API_BASE_URL = 'http://192.168.8.100:3000'; // Home network for mudabbir since, server is on my home network and router dont do loopback!!!
|
//export const API_BASE_URL = 'http://192.168.8.100:3000'; // Home network for mudabbir since, server is on my home network and router dont do loopback!!!
|
||||||
const API = {};
|
const API = {};
|
||||||
|
|
||||||
API.get = (endpoint) => callFetch(endpoint, 'GET');
|
API.get = (endpoint) => callFetch(endpoint, "GET");
|
||||||
API.post = (endpoint, data) => callFetch(endpoint, 'POST', data);
|
API.post = (endpoint, data) => callFetch(endpoint, "POST", data);
|
||||||
API.put = (endpoint, data) => callFetch(endpoint, 'PUT', data);
|
API.put = (endpoint, data) => callFetch(endpoint, "PUT", data);
|
||||||
API.delete = (endpoint) => callFetch(endpoint, 'DELETE');
|
API.delete = (endpoint) => callFetch(endpoint, "DELETE");
|
||||||
|
|
||||||
export default API;
|
export default API;
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ const callFetch = async (endpoint, method, dataObj = null) => {
|
|||||||
if (dataObj)
|
if (dataObj)
|
||||||
requestObj = {
|
requestObj = {
|
||||||
...requestObj,
|
...requestObj,
|
||||||
headers: {'Content-type': 'application/json'},
|
headers: { "Content-type": "application/json" },
|
||||||
body: JSON.stringify(dataObj),
|
body: JSON.stringify(dataObj),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -31,4 +31,3 @@ const callFetch = async (endpoint, method, dataObj = null) => {
|
|||||||
return { isSuccess: false, message: error.message };
|
return { isSuccess: false, message: error.message };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1,14 +1,80 @@
|
|||||||
import {Alert, Pressable, StyleSheet, Text, View} from 'react-native';
|
import { Alert, Pressable, StyleSheet, Text, View } from "react-native";
|
||||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import {clearGameSession, getSession} from '../../hooks/SessionStore';
|
import { clearGameSession, getSession } from "../../hooks/SessionStore";
|
||||||
import API, {API_BASE_URL} from '../API/API';
|
import API, { API_BASE_URL } from "../API/API";
|
||||||
|
|
||||||
|
const BottomNavbar = ({ navigation, routeName }) => {
|
||||||
|
// Initialisation ------------
|
||||||
const BottomNavbar = ({navigation, routeName, routeParams}) => {
|
const BottomNavbar = ({navigation, routeName, routeParams}) => {
|
||||||
// Initialisation ------------
|
// Initialisation ------------
|
||||||
|
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const session = getSession();
|
const session = getSession();
|
||||||
|
|
||||||
|
const adminTabs = [
|
||||||
|
{
|
||||||
|
label: "Settings",
|
||||||
|
route: "GameSettingsScreen",
|
||||||
|
onPress: () => navigation.navigate("GameSettingsScreen"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Map",
|
||||||
|
route: "MapScreen",
|
||||||
|
onPress: () => navigation.navigate("MapScreen"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Players",
|
||||||
|
route: "PlayersScreen",
|
||||||
|
onPress: () => navigation.navigate("PlayersScreen"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Leaderboard",
|
||||||
|
route: "LeaderboardScreen",
|
||||||
|
onPress: () => navigation.navigate("LeaderboardScreen"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Games",
|
||||||
|
route: "GameSelectionScreen",
|
||||||
|
onPress: () => navigation.navigate("GameSelectionScreen"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Global",
|
||||||
|
route: "GlobalEventsScreen",
|
||||||
|
onPress: () => navigation.navigate("GlobalEventsScreen"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const playerTabs = [
|
||||||
|
{
|
||||||
|
label: "Team",
|
||||||
|
route: "TeamScreen",
|
||||||
|
onPress: () => navigation.navigate("TeamScreen"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Map",
|
||||||
|
route: "MapScreen",
|
||||||
|
onPress: () => navigation.navigate("MapScreen"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Leaderboard",
|
||||||
|
route: "LeaderboardScreen",
|
||||||
|
onPress: () => navigation.navigate("LeaderboardScreen"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Games",
|
||||||
|
route: "GameSelectionScreen",
|
||||||
|
onPress: () => navigation.navigate("GameSelectionScreen"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Global",
|
||||||
|
route: "GlobalEventsScreen",
|
||||||
|
onPress: () => navigation.navigate("GlobalEventsScreen"),
|
||||||
|
},
|
||||||
|
|
||||||
|
{ label: "Leave", route: "__leave__", onPress: () => handleLeaveGame() },
|
||||||
|
];
|
||||||
|
|
||||||
|
const tabs = session.isAcceptedAdmin ? adminTabs : playerTabs;
|
||||||
const selectedDepartment = routeName === 'DepartmentSettingsScreen' && routeParams?.SGid
|
const selectedDepartment = routeName === 'DepartmentSettingsScreen' && routeParams?.SGid
|
||||||
? {SGid: routeParams.SGid, SubGroupName: routeParams.SubGroupName || routeParams.selectedDepartmentName || 'Department'}
|
? {SGid: routeParams.SGid, SubGroupName: routeParams.SubGroupName || routeParams.selectedDepartmentName || 'Department'}
|
||||||
: null;
|
: null;
|
||||||
@@ -55,6 +121,27 @@ const BottomNavbar = ({navigation, routeName, routeParams}) => {
|
|||||||
// State ----------------------
|
// State ----------------------
|
||||||
// Handlers -------------------
|
// Handlers -------------------
|
||||||
|
|
||||||
|
const handleLeaveGame = () => {
|
||||||
|
Alert.alert("Leave Game", "Are you sure you want to leave this game?", [
|
||||||
|
{ text: "Cancel", style: "cancel" },
|
||||||
|
{
|
||||||
|
text: "Leave",
|
||||||
|
style: "destructive",
|
||||||
|
onPress: async () => {
|
||||||
|
const s = getSession();
|
||||||
|
const endpoint = `${API_BASE_URL}/subgroup-memberships?Uid=${s.currentUid}&Gid=${s.currentGid}`;
|
||||||
|
const res = await API.get(endpoint);
|
||||||
|
if (res.isSuccess && res.result.length > 0) {
|
||||||
|
await API.delete(
|
||||||
|
`${API_BASE_URL}/subgroup-memberships/${res.result[0].id}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
clearGameSession();
|
||||||
|
navigation.reset({ index: 0, routes: [{ name: "Game" }] });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
const handleLeaveGame = () => {
|
const handleLeaveGame = () => {
|
||||||
Alert.alert('Leave Game', 'Are you sure you want to leave this game?', [
|
Alert.alert('Leave Game', 'Are you sure you want to leave this game?', [
|
||||||
{text: 'Cancel', style: 'cancel'},
|
{text: 'Cancel', style: 'cancel'},
|
||||||
@@ -77,6 +164,74 @@ const BottomNavbar = ({navigation, routeName, routeParams}) => {
|
|||||||
|
|
||||||
// View -----------------------
|
// View -----------------------
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.container, { paddingBottom: insets.bottom }]}>
|
||||||
|
{tabs.map((tab) => {
|
||||||
|
const isActive = routeName === tab.route;
|
||||||
|
const isLeave = tab.route === "__leave__";
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
key={tab.label}
|
||||||
|
onPress={tab.onPress}
|
||||||
|
style={[
|
||||||
|
styles.tab,
|
||||||
|
isActive && styles.tabActive,
|
||||||
|
isLeave && styles.tabLeave,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.tabText,
|
||||||
|
isActive && styles.tabTextActive,
|
||||||
|
isLeave && styles.tabTextLeave,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
minHeight: 64,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: "#555555",
|
||||||
|
backgroundColor: "#4a4a4a",
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-around",
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
},
|
||||||
|
tab: {
|
||||||
|
flex: 1,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
paddingVertical: 12,
|
||||||
|
borderRadius: 8,
|
||||||
|
marginHorizontal: 4,
|
||||||
|
},
|
||||||
|
tabActive: {
|
||||||
|
backgroundColor: "#5c5c5c",
|
||||||
|
},
|
||||||
|
tabLeave: {
|
||||||
|
backgroundColor: "rgba(220,38,38,0.15)",
|
||||||
|
},
|
||||||
|
tabText: {
|
||||||
|
color: "#d1d5db",
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: "600",
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
tabTextActive: {
|
||||||
|
color: "#ffffff",
|
||||||
|
},
|
||||||
|
tabTextLeave: {
|
||||||
|
color: "#fca5a5",
|
||||||
|
},
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, {paddingBottom: insets.bottom}]}>
|
<View style={[styles.container, {paddingBottom: insets.bottom}]}>
|
||||||
{visibleTabs.map((tab) => {
|
{visibleTabs.map((tab) => {
|
||||||
|
|||||||
@@ -17,24 +17,47 @@ const Screen = ({ children, style, showBack = false }) => {
|
|||||||
TeamScreen: "Current Team",
|
TeamScreen: "Current Team",
|
||||||
MapScreen: "Map",
|
MapScreen: "Map",
|
||||||
GameSettingsScreen: "Game Settings",
|
GameSettingsScreen: "Game Settings",
|
||||||
|
GameSelectionScreen: "Games",
|
||||||
DepartmentSettingsScreen: "Department Settings",
|
DepartmentSettingsScreen: "Department Settings",
|
||||||
PlayersScreen: "Players",
|
PlayersScreen: "Players",
|
||||||
ExpandedMapScreen: "Map View",
|
ExpandedMapScreen: "Map View",
|
||||||
|
GlobalEventsScreen: "Global Events",
|
||||||
|
GlobalMapScreen: "Global Map",
|
||||||
|
GlobalCacheViewScreen: "Cache Details",
|
||||||
|
GlobalLeaderboardScreen: "Global Leaderboard",
|
||||||
};
|
};
|
||||||
|
|
||||||
// State ----------------------
|
// State ----------------------
|
||||||
|
|
||||||
const routeName = route.name;
|
const routeName = route.name;
|
||||||
const isAuthScreen = routeName === "LoginScreen" || routeName === "RegisterScreen";
|
const isAuthScreen =
|
||||||
|
routeName === "LoginScreen" || routeName === "RegisterScreen";
|
||||||
const pageTitle = titleMap[routeName] || "Treasure Hunt";
|
const pageTitle = titleMap[routeName] || "Treasure Hunt";
|
||||||
|
|
||||||
// Handlers -------------------
|
// Handlers -------------------
|
||||||
// View -----------------------
|
// View -----------------------
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.screen, isAuthScreen && {paddingTop: insets.top, paddingBottom: insets.bottom}]}>
|
<View
|
||||||
{!isAuthScreen && <TopNavbar title={pageTitle} showBack={showBack} navigation={navigation} />}
|
style={[
|
||||||
|
styles.screen,
|
||||||
|
isAuthScreen && {
|
||||||
|
paddingTop: insets.top,
|
||||||
|
paddingBottom: insets.bottom,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{!isAuthScreen && (
|
||||||
|
<TopNavbar
|
||||||
|
title={pageTitle}
|
||||||
|
showBack={showBack}
|
||||||
|
navigation={navigation}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<View style={[styles.content, style]}>{children}</View>
|
<View style={[styles.content, style]}>{children}</View>
|
||||||
|
{!isAuthScreen && (
|
||||||
|
<BottomNavbar navigation={navigation} routeName={routeName} />
|
||||||
|
)}
|
||||||
{!isAuthScreen && <BottomNavbar navigation={navigation} routeName={routeName} routeParams={route.params} />}
|
{!isAuthScreen && <BottomNavbar navigation={navigation} routeName={routeName} routeParams={route.params} />}
|
||||||
<StatusBar style="light" />
|
<StatusBar style="light" />
|
||||||
</View>
|
</View>
|
||||||
@@ -55,4 +78,3 @@ const styles = StyleSheet.create({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default Screen;
|
export default Screen;
|
||||||
|
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
import { ScrollView, StyleSheet, Text, View } from "react-native";
|
||||||
|
import Screen from "../layout/Screen";
|
||||||
|
import Card from "../UI/Card";
|
||||||
|
import { Button, ButtonTray } from "../UI/Button";
|
||||||
|
import { getSession } from "../../hooks/SessionStore";
|
||||||
|
|
||||||
|
const GameSelectionScreen = ({ navigation }) => {
|
||||||
|
// Initialisations ---------------------
|
||||||
|
|
||||||
|
const session = getSession();
|
||||||
|
const isAdmin = Boolean(session.isAcceptedAdmin);
|
||||||
|
|
||||||
|
const privateGames = [
|
||||||
|
{
|
||||||
|
id: "active-game",
|
||||||
|
name: "Private Game",
|
||||||
|
players: "4 players",
|
||||||
|
endsIn: "Ends in 2h 15m",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// State -------------------------------
|
||||||
|
// Handlers ----------------------------
|
||||||
|
|
||||||
|
const handleCreateGame = () => {
|
||||||
|
if (isAdmin) {
|
||||||
|
navigation.navigate("GameSettingsScreen");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigation.navigate("TeamScreen");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenGame = () => {
|
||||||
|
navigation.navigate("TeamScreen");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExplore = () => {
|
||||||
|
navigation.navigate("GlobalEventsScreen");
|
||||||
|
};
|
||||||
|
|
||||||
|
// View --------------------------------
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Screen style={styles.screenContent}>
|
||||||
|
<ScrollView contentContainerStyle={styles.scrollContent}>
|
||||||
|
<View style={styles.sectionTop}>
|
||||||
|
<Text style={styles.sectionTitle}>Your Games</Text>
|
||||||
|
<ButtonTray>
|
||||||
|
<Button
|
||||||
|
label={isAdmin ? "Create Game" : "Manage Team"}
|
||||||
|
onClick={handleCreateGame}
|
||||||
|
styleButton={styles.primaryButton}
|
||||||
|
styleLabel={styles.primaryButtonLabel}
|
||||||
|
/>
|
||||||
|
</ButtonTray>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.sectionBlock}>
|
||||||
|
<Text style={styles.blockTitle}>Private Games</Text>
|
||||||
|
{privateGames.map((game) => (
|
||||||
|
<Card key={game.id} style={styles.gameCard}>
|
||||||
|
<Text style={styles.gameName}>{game.name}</Text>
|
||||||
|
<Text style={styles.gameMeta}>{game.players}</Text>
|
||||||
|
<Text style={styles.gameMeta}>{game.endsIn}</Text>
|
||||||
|
<View style={styles.cardActions}>
|
||||||
|
<Button
|
||||||
|
label="Show"
|
||||||
|
onClick={handleOpenGame}
|
||||||
|
styleButton={styles.secondaryButton}
|
||||||
|
styleLabel={styles.secondaryButtonLabel}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.sectionBlock}>
|
||||||
|
<Text style={styles.blockTitle}>Global Mode</Text>
|
||||||
|
<Text style={styles.supportText}>
|
||||||
|
Explore all visible caches and live activity from the map.
|
||||||
|
</Text>
|
||||||
|
<ButtonTray>
|
||||||
|
<Button
|
||||||
|
label="Explore"
|
||||||
|
onClick={handleExplore}
|
||||||
|
styleButton={styles.neutralButton}
|
||||||
|
styleLabel={styles.neutralButtonLabel}
|
||||||
|
/>
|
||||||
|
</ButtonTray>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</Screen>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
screenContent: {
|
||||||
|
backgroundColor: "#f3f4f6",
|
||||||
|
paddingHorizontal: 0,
|
||||||
|
paddingVertical: 0,
|
||||||
|
},
|
||||||
|
scrollContent: {
|
||||||
|
padding: 15,
|
||||||
|
paddingBottom: 28,
|
||||||
|
},
|
||||||
|
sectionTop: {
|
||||||
|
marginBottom: 16,
|
||||||
|
gap: 10,
|
||||||
|
},
|
||||||
|
sectionTitle: {
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: "700",
|
||||||
|
color: "#111827",
|
||||||
|
},
|
||||||
|
sectionBlock: {
|
||||||
|
marginTop: 8,
|
||||||
|
marginBottom: 10,
|
||||||
|
},
|
||||||
|
blockTitle: {
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: "700",
|
||||||
|
color: "#374151",
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
gameCard: {
|
||||||
|
marginBottom: 10,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 14,
|
||||||
|
borderColor: "#d1d5db",
|
||||||
|
borderRadius: 10,
|
||||||
|
},
|
||||||
|
gameName: {
|
||||||
|
fontSize: 17,
|
||||||
|
fontWeight: "700",
|
||||||
|
color: "#111827",
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
gameMeta: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#4b5563",
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
cardActions: {
|
||||||
|
marginTop: 12,
|
||||||
|
maxWidth: 160,
|
||||||
|
},
|
||||||
|
supportText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#4b5563",
|
||||||
|
marginBottom: 10,
|
||||||
|
lineHeight: 20,
|
||||||
|
},
|
||||||
|
primaryButton: {
|
||||||
|
backgroundColor: "#16a34a",
|
||||||
|
borderColor: "#16a34a",
|
||||||
|
},
|
||||||
|
primaryButtonLabel: {
|
||||||
|
color: "#ffffff",
|
||||||
|
fontWeight: "600",
|
||||||
|
},
|
||||||
|
secondaryButton: {
|
||||||
|
backgroundColor: "#2563eb",
|
||||||
|
borderColor: "#2563eb",
|
||||||
|
flex: 0,
|
||||||
|
minHeight: 38,
|
||||||
|
paddingHorizontal: 18,
|
||||||
|
},
|
||||||
|
secondaryButtonLabel: {
|
||||||
|
color: "#ffffff",
|
||||||
|
fontWeight: "600",
|
||||||
|
fontSize: 14,
|
||||||
|
},
|
||||||
|
neutralButton: {
|
||||||
|
backgroundColor: "#374151",
|
||||||
|
borderColor: "#374151",
|
||||||
|
maxWidth: 180,
|
||||||
|
},
|
||||||
|
neutralButtonLabel: {
|
||||||
|
color: "#ffffff",
|
||||||
|
fontWeight: "600",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default GameSelectionScreen;
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { ActivityIndicator, Alert, StyleSheet, Text, View } from "react-native";
|
||||||
|
import Screen from "../../layout/Screen";
|
||||||
|
import { Button, ButtonTray } from "../../UI/Button";
|
||||||
|
import Card from "../../UI/Card";
|
||||||
|
import useGlobalHook from "../../../hooks/useGlobalHook";
|
||||||
|
import { getSession } from "../../../hooks/SessionStore";
|
||||||
|
|
||||||
|
const GlobalCacheViewScreen = ({ navigation, route }) => {
|
||||||
|
// Initialisations ---------------------
|
||||||
|
|
||||||
|
const { cache, isFound: initIsFound, onFindLogged } = route.params;
|
||||||
|
const { logFind } = useGlobalHook();
|
||||||
|
const session = getSession();
|
||||||
|
|
||||||
|
// State -------------------------------
|
||||||
|
|
||||||
|
const [isFound, setIsFound] = useState(initIsFound);
|
||||||
|
const [isLogging, setIsLogging] = useState(false);
|
||||||
|
|
||||||
|
// Handlers ----------------------------
|
||||||
|
|
||||||
|
const handleLogFind = async () => {
|
||||||
|
if (!session.currentGlobalPlayerId) {
|
||||||
|
Alert.alert(
|
||||||
|
"Not joined",
|
||||||
|
"You must join the event before logging a find.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Alert.alert(
|
||||||
|
"Log Discovery",
|
||||||
|
`Record that you found "${cache.CacheName}" and earn ${cache.CachePoints ?? 0} points?`,
|
||||||
|
[
|
||||||
|
{ text: "Cancel", style: "cancel" },
|
||||||
|
{
|
||||||
|
text: "Log Find",
|
||||||
|
onPress: async () => {
|
||||||
|
setIsLogging(true);
|
||||||
|
const result = await logFind({
|
||||||
|
FindPlayerID: session.currentGlobalPlayerId,
|
||||||
|
FindCacheID: cache.CacheID,
|
||||||
|
FindDatetime: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
setIsLogging(false);
|
||||||
|
if (result) {
|
||||||
|
setIsFound(true);
|
||||||
|
if (onFindLogged) onFindLogged();
|
||||||
|
Alert.alert(
|
||||||
|
"Discovery logged!",
|
||||||
|
`+${cache.CachePoints ?? 0} points earned.`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Alert.alert("Error", "Could not log find. Please try again.");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// View --------------------------------
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Screen showBack>
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Card style={styles.card}>
|
||||||
|
<Text style={styles.title}>{cache.CacheName}</Text>
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.statusBadge,
|
||||||
|
isFound ? styles.badgeFound : styles.badgeOpen,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text style={styles.statusText}>
|
||||||
|
{isFound ? "Already Found" : "Not Yet Found"}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.detailRow}>
|
||||||
|
<Text style={styles.label}>Points</Text>
|
||||||
|
<Text style={styles.value}>{cache.CachePoints ?? 0}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{cache.CacheDescription ? (
|
||||||
|
<View style={styles.detailRow}>
|
||||||
|
<Text style={styles.label}>Description</Text>
|
||||||
|
<Text style={styles.value}>{cache.CacheDescription}</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{cache.CacheClue ? (
|
||||||
|
<View style={styles.detailRow}>
|
||||||
|
<Text style={styles.label}>Clue</Text>
|
||||||
|
<Text style={styles.value}>{cache.CacheClue}</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{isLogging ? (
|
||||||
|
<ActivityIndicator size="large" />
|
||||||
|
) : (
|
||||||
|
<ButtonTray>
|
||||||
|
{!isFound && (
|
||||||
|
<Button
|
||||||
|
label="Log Discovery"
|
||||||
|
onClick={handleLogFind}
|
||||||
|
styleButton={styles.logButton}
|
||||||
|
styleLabel={styles.logButtonLabel}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Button label="Back" onClick={() => navigation.goBack()} />
|
||||||
|
</ButtonTray>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</Screen>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
gap: 16,
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
gap: 12,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: "700",
|
||||||
|
color: "#111827",
|
||||||
|
},
|
||||||
|
statusBadge: {
|
||||||
|
alignSelf: "flex-start",
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
paddingVertical: 4,
|
||||||
|
borderRadius: 12,
|
||||||
|
},
|
||||||
|
badgeFound: {
|
||||||
|
backgroundColor: "#d1fae5",
|
||||||
|
},
|
||||||
|
badgeOpen: {
|
||||||
|
backgroundColor: "#fef3c7",
|
||||||
|
},
|
||||||
|
statusText: {
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#374151",
|
||||||
|
},
|
||||||
|
detailRow: {
|
||||||
|
gap: 2,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#6b7280",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
fontSize: 15,
|
||||||
|
color: "#111827",
|
||||||
|
},
|
||||||
|
logButton: {
|
||||||
|
backgroundColor: "#16a34a",
|
||||||
|
borderColor: "#16a34a",
|
||||||
|
flex: 2,
|
||||||
|
},
|
||||||
|
logButtonLabel: {
|
||||||
|
color: "#fff",
|
||||||
|
fontWeight: "700",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default GlobalCacheViewScreen;
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { ActivityIndicator, StyleSheet, Text, View } from "react-native";
|
||||||
|
import Screen from "../../layout/Screen";
|
||||||
|
import { Button, ButtonTray } from "../../UI/Button";
|
||||||
|
import EventList from "../../../entity/event/EventList";
|
||||||
|
import useGlobalHook from "../../../hooks/useGlobalHook";
|
||||||
|
import { getSession, setGlobalSession } from "../../../hooks/SessionStore";
|
||||||
|
|
||||||
|
const GlobalEventsScreen = ({ navigation }) => {
|
||||||
|
// Initialisations ---------------------
|
||||||
|
|
||||||
|
const { getPublicEvents, getPlayersByEvent, joinEvent } = useGlobalHook();
|
||||||
|
const session = getSession();
|
||||||
|
|
||||||
|
// State -------------------------------
|
||||||
|
|
||||||
|
const [events, setEvents] = useState([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [joining, setJoining] = useState(null);
|
||||||
|
|
||||||
|
// Handlers ----------------------------
|
||||||
|
|
||||||
|
const loadEvents = useCallback(async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError("");
|
||||||
|
const data = await getPublicEvents();
|
||||||
|
setEvents(data);
|
||||||
|
setIsLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadEvents();
|
||||||
|
}, [loadEvents]);
|
||||||
|
|
||||||
|
const handleSelect = async (event) => {
|
||||||
|
setError("");
|
||||||
|
setJoining(event.EventID);
|
||||||
|
|
||||||
|
// Check if already a player
|
||||||
|
const players = await getPlayersByEvent(event.EventID);
|
||||||
|
const existing = (players || []).find(
|
||||||
|
(p) => p.PlayerUserID === session.currentUid,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
setGlobalSession(event.EventID, existing.PlayerID);
|
||||||
|
navigation.navigate("GlobalMapScreen", { eventId: event.EventID });
|
||||||
|
} else {
|
||||||
|
// Join the event
|
||||||
|
const player = await joinEvent({
|
||||||
|
PlayerUserID: session.currentUid,
|
||||||
|
PlayerEventID: event.EventID,
|
||||||
|
});
|
||||||
|
if (player) {
|
||||||
|
setGlobalSession(event.EventID, player.PlayerID);
|
||||||
|
navigation.navigate("GlobalMapScreen", { eventId: event.EventID });
|
||||||
|
} else {
|
||||||
|
setError("Failed to join event. Please try again.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setJoining(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// View --------------------------------
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Screen>
|
||||||
|
<View style={styles.container}>
|
||||||
|
<ButtonTray>
|
||||||
|
<Button label="Refresh" onClick={loadEvents} />
|
||||||
|
</ButtonTray>
|
||||||
|
|
||||||
|
{error ? <Text style={styles.error}>{error}</Text> : null}
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<View style={styles.center}>
|
||||||
|
<ActivityIndicator size="large" />
|
||||||
|
<Text style={styles.loadingText}>Loading public events…</Text>
|
||||||
|
</View>
|
||||||
|
) : events.length === 0 ? (
|
||||||
|
<Text style={styles.empty}>No public events available.</Text>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Text style={styles.hint}>
|
||||||
|
Tap an event to join and explore its caches.
|
||||||
|
</Text>
|
||||||
|
<EventList events={events} onSelect={handleSelect} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{joining !== null && (
|
||||||
|
<View style={styles.overlay}>
|
||||||
|
<ActivityIndicator size="large" color="#fff" />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</Screen>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
gap: 12,
|
||||||
|
},
|
||||||
|
center: {
|
||||||
|
flex: 1,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 10,
|
||||||
|
},
|
||||||
|
loadingText: {
|
||||||
|
color: "#6b7280",
|
||||||
|
fontSize: 14,
|
||||||
|
},
|
||||||
|
empty: {
|
||||||
|
textAlign: "center",
|
||||||
|
color: "#9ca3af",
|
||||||
|
marginTop: 30,
|
||||||
|
fontSize: 15,
|
||||||
|
},
|
||||||
|
hint: {
|
||||||
|
fontSize: 13,
|
||||||
|
color: "#6b7280",
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
color: "#dc2626",
|
||||||
|
fontSize: 14,
|
||||||
|
},
|
||||||
|
overlay: {
|
||||||
|
...StyleSheet.absoluteFillObject,
|
||||||
|
backgroundColor: "rgba(0,0,0,0.35)",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default GlobalEventsScreen;
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
|
ScrollView,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
View,
|
||||||
|
} from "react-native";
|
||||||
|
import Screen from "../../layout/Screen";
|
||||||
|
import { Button, ButtonTray } from "../../UI/Button";
|
||||||
|
import Card from "../../UI/Card";
|
||||||
|
import FindList from "../../../entity/find/FindList";
|
||||||
|
import useGlobalHook from "../../../hooks/useGlobalHook";
|
||||||
|
import { getSession } from "../../../hooks/SessionStore";
|
||||||
|
|
||||||
|
const GlobalLeaderboardScreen = ({ navigation, route }) => {
|
||||||
|
// Initialisations ---------------------
|
||||||
|
|
||||||
|
const { eventId } = route.params;
|
||||||
|
const { getFindsByEvent, getCachesByEvent, getPlayersByEvent } =
|
||||||
|
useGlobalHook();
|
||||||
|
const session = getSession();
|
||||||
|
|
||||||
|
// State -------------------------------
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [rankings, setRankings] = useState([]);
|
||||||
|
const [recentFinds, setRecentFinds] = useState([]);
|
||||||
|
const [activeTab, setActiveTab] = useState("rankings");
|
||||||
|
|
||||||
|
// Handlers ----------------------------
|
||||||
|
|
||||||
|
const loadData = useCallback(async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const [finds, caches, players] = await Promise.all([
|
||||||
|
getFindsByEvent(eventId),
|
||||||
|
getCachesByEvent(eventId),
|
||||||
|
getPlayersByEvent(eventId),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Build cache points lookup
|
||||||
|
const cacheMap = {};
|
||||||
|
(caches || []).forEach((c) => {
|
||||||
|
cacheMap[c.CacheID] = c;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Augment finds with cache info for FindList
|
||||||
|
const augmented = (finds || []).map((f) => ({
|
||||||
|
...f,
|
||||||
|
FindCache: f.FindCache || cacheMap[f.FindCacheID] || null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Sort recent first
|
||||||
|
const sorted = [...augmented].sort(
|
||||||
|
(a, b) => new Date(b.FindDatetime) - new Date(a.FindDatetime),
|
||||||
|
);
|
||||||
|
setRecentFinds(sorted.slice(0, 50));
|
||||||
|
|
||||||
|
// Aggregate points per player
|
||||||
|
const pointsMap = {};
|
||||||
|
augmented.forEach((f) => {
|
||||||
|
const pid = f.FindPlayerID;
|
||||||
|
const pts = f.FindCache?.CachePoints ?? 0;
|
||||||
|
if (!pointsMap[pid])
|
||||||
|
pointsMap[pid] = { playerId: pid, points: 0, finds: 0, name: null };
|
||||||
|
pointsMap[pid].points += pts;
|
||||||
|
pointsMap[pid].finds += 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Attach usernames from players list
|
||||||
|
(players || []).forEach((p) => {
|
||||||
|
if (pointsMap[p.PlayerID]) {
|
||||||
|
pointsMap[p.PlayerID].name =
|
||||||
|
p.PlayerUser?.UserUsername || `Player #${p.PlayerUserID}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const ranked = Object.values(pointsMap).sort((a, b) => b.points - a.points);
|
||||||
|
setRankings(ranked);
|
||||||
|
setIsLoading(false);
|
||||||
|
}, [eventId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadData();
|
||||||
|
}, [loadData]);
|
||||||
|
|
||||||
|
// View --------------------------------
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Screen style={styles.center}>
|
||||||
|
<ActivityIndicator size="large" />
|
||||||
|
</Screen>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Screen showBack>
|
||||||
|
<View style={styles.container}>
|
||||||
|
<View style={styles.tabs}>
|
||||||
|
<TabButton
|
||||||
|
label="Rankings"
|
||||||
|
active={activeTab === "rankings"}
|
||||||
|
onPress={() => setActiveTab("rankings")}
|
||||||
|
/>
|
||||||
|
<TabButton
|
||||||
|
label="Recent Finds"
|
||||||
|
active={activeTab === "recentFinds"}
|
||||||
|
onPress={() => setActiveTab("recentFinds")}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{activeTab === "rankings" ? (
|
||||||
|
<ScrollView contentContainerStyle={styles.rankList}>
|
||||||
|
{rankings.length === 0 ? (
|
||||||
|
<Text style={styles.empty}>No finds recorded yet.</Text>
|
||||||
|
) : (
|
||||||
|
rankings.map((entry, index) => {
|
||||||
|
const isCurrent =
|
||||||
|
entry.playerId === session.currentGlobalPlayerId;
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
key={entry.playerId}
|
||||||
|
style={[styles.rankCard, isCurrent && styles.myCard]}
|
||||||
|
>
|
||||||
|
<View style={styles.rankRow}>
|
||||||
|
<Text style={styles.rankPos}>#{index + 1}</Text>
|
||||||
|
<Text style={styles.rankName} numberOfLines={1}>
|
||||||
|
{entry.name || `Player #${entry.playerId}`}
|
||||||
|
{isCurrent ? " (you)" : ""}
|
||||||
|
</Text>
|
||||||
|
<View style={styles.rankRight}>
|
||||||
|
<Text style={styles.rankPoints}>
|
||||||
|
{entry.points} pts
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.rankFinds}>
|
||||||
|
{entry.finds} finds
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
) : recentFinds.length === 0 ? (
|
||||||
|
<Text style={styles.empty}>No finds recorded yet.</Text>
|
||||||
|
) : (
|
||||||
|
<FindList finds={recentFinds} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ButtonTray>
|
||||||
|
<Button label="Refresh" onClick={loadData} />
|
||||||
|
</ButtonTray>
|
||||||
|
</View>
|
||||||
|
</Screen>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TabButton = ({ label, active, onPress }) => (
|
||||||
|
<Text
|
||||||
|
onPress={onPress}
|
||||||
|
style={[tabStyles.tab, active && tabStyles.activeTab]}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
|
||||||
|
const tabStyles = StyleSheet.create({
|
||||||
|
tab: {
|
||||||
|
flex: 1,
|
||||||
|
textAlign: "center",
|
||||||
|
paddingVertical: 8,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#6b7280",
|
||||||
|
borderBottomWidth: 2,
|
||||||
|
borderBottomColor: "transparent",
|
||||||
|
},
|
||||||
|
activeTab: {
|
||||||
|
color: "#111827",
|
||||||
|
borderBottomColor: "#111827",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
gap: 10,
|
||||||
|
},
|
||||||
|
center: {
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
tabs: {
|
||||||
|
flexDirection: "row",
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: "#e5e7eb",
|
||||||
|
},
|
||||||
|
rankList: {
|
||||||
|
gap: 6,
|
||||||
|
paddingBottom: 8,
|
||||||
|
},
|
||||||
|
rankCard: {
|
||||||
|
marginBottom: 0,
|
||||||
|
},
|
||||||
|
myCard: {
|
||||||
|
borderColor: "#2563eb",
|
||||||
|
borderWidth: 2,
|
||||||
|
},
|
||||||
|
rankRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 10,
|
||||||
|
},
|
||||||
|
rankPos: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "700",
|
||||||
|
color: "#6b7280",
|
||||||
|
width: 32,
|
||||||
|
},
|
||||||
|
rankName: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#111827",
|
||||||
|
},
|
||||||
|
rankRight: {
|
||||||
|
alignItems: "flex-end",
|
||||||
|
},
|
||||||
|
rankPoints: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "700",
|
||||||
|
color: "#16a34a",
|
||||||
|
},
|
||||||
|
rankFinds: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: "#9ca3af",
|
||||||
|
},
|
||||||
|
empty: {
|
||||||
|
textAlign: "center",
|
||||||
|
color: "#9ca3af",
|
||||||
|
marginTop: 30,
|
||||||
|
fontSize: 15,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default GlobalLeaderboardScreen;
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { ActivityIndicator, StyleSheet, Text, View } from "react-native";
|
||||||
|
import MapView, { Marker } from "react-native-maps";
|
||||||
|
import * as Location from "expo-location";
|
||||||
|
import Screen from "../../layout/Screen";
|
||||||
|
import { Button, ButtonTray } from "../../UI/Button";
|
||||||
|
import CacheList from "../../../entity/cache/CacheList";
|
||||||
|
import useGlobalHook from "../../../hooks/useGlobalHook";
|
||||||
|
import { getSession } from "../../../hooks/SessionStore";
|
||||||
|
|
||||||
|
const DEFAULT_REGION = {
|
||||||
|
latitude: 51.5074,
|
||||||
|
longitude: -0.1278,
|
||||||
|
latitudeDelta: 0.05,
|
||||||
|
longitudeDelta: 0.05,
|
||||||
|
};
|
||||||
|
|
||||||
|
const GlobalMapScreen = ({ navigation, route }) => {
|
||||||
|
// Initialisations ---------------------
|
||||||
|
|
||||||
|
const { eventId } = route.params;
|
||||||
|
const { getCachesByEvent, getFindsByPlayer } = useGlobalHook();
|
||||||
|
const session = getSession();
|
||||||
|
|
||||||
|
// State -------------------------------
|
||||||
|
|
||||||
|
const [caches, setCaches] = useState([]);
|
||||||
|
const [foundCacheIds, setFoundCacheIds] = useState([]);
|
||||||
|
const [userLocation, setUserLocation] = useState(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [activeTab, setActiveTab] = useState("map");
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
// Handlers ----------------------------
|
||||||
|
|
||||||
|
const loadData = useCallback(async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError("");
|
||||||
|
|
||||||
|
const [cacheData, findsData] = await Promise.all([
|
||||||
|
getCachesByEvent(eventId),
|
||||||
|
session.currentGlobalPlayerId
|
||||||
|
? getFindsByPlayer(session.currentGlobalPlayerId)
|
||||||
|
: Promise.resolve([]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
setCaches(cacheData);
|
||||||
|
setFoundCacheIds((findsData || []).map((f) => f.FindCacheID));
|
||||||
|
setIsLoading(false);
|
||||||
|
}, [eventId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadData();
|
||||||
|
}, [loadData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let sub;
|
||||||
|
const start = async () => {
|
||||||
|
const { status } = await Location.requestForegroundPermissionsAsync();
|
||||||
|
if (status !== "granted") {
|
||||||
|
setError("Location permission denied");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const last = await Location.getLastKnownPositionAsync();
|
||||||
|
if (last) {
|
||||||
|
setUserLocation({
|
||||||
|
latitude: last.coords.latitude,
|
||||||
|
longitude: last.coords.longitude,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const pos = await Location.getCurrentPositionAsync({
|
||||||
|
accuracy: Location.Accuracy.Low,
|
||||||
|
});
|
||||||
|
setUserLocation({
|
||||||
|
latitude: pos.coords.latitude,
|
||||||
|
longitude: pos.coords.longitude,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sub = await Location.watchPositionAsync(
|
||||||
|
{
|
||||||
|
accuracy: Location.Accuracy.Balanced,
|
||||||
|
distanceInterval: 5,
|
||||||
|
timeInterval: 3000,
|
||||||
|
},
|
||||||
|
(pos) =>
|
||||||
|
setUserLocation({
|
||||||
|
latitude: pos.coords.latitude,
|
||||||
|
longitude: pos.coords.longitude,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
start();
|
||||||
|
return () => {
|
||||||
|
if (sub) sub.remove();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const mapRegion = userLocation
|
||||||
|
? { ...userLocation, latitudeDelta: 0.01, longitudeDelta: 0.01 }
|
||||||
|
: DEFAULT_REGION;
|
||||||
|
|
||||||
|
const handleCacheSelect = (cache) => {
|
||||||
|
navigation.navigate("GlobalCacheViewScreen", {
|
||||||
|
cache,
|
||||||
|
isFound: foundCacheIds.includes(cache.CacheID),
|
||||||
|
onFindLogged: loadData,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGotoLeaderboard = () => {
|
||||||
|
navigation.navigate("GlobalLeaderboardScreen", { eventId });
|
||||||
|
};
|
||||||
|
|
||||||
|
// View --------------------------------
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Screen style={styles.center}>
|
||||||
|
<ActivityIndicator size="large" />
|
||||||
|
</Screen>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Screen showBack>
|
||||||
|
<View style={styles.container}>
|
||||||
|
{/* Tab switcher */}
|
||||||
|
<View style={styles.tabs}>
|
||||||
|
<TabButton
|
||||||
|
label="Map"
|
||||||
|
active={activeTab === "map"}
|
||||||
|
onPress={() => setActiveTab("map")}
|
||||||
|
/>
|
||||||
|
<TabButton
|
||||||
|
label="List"
|
||||||
|
active={activeTab === "list"}
|
||||||
|
onPress={() => setActiveTab("list")}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{error ? <Text style={styles.error}>{error}</Text> : null}
|
||||||
|
|
||||||
|
{activeTab === "map" ? (
|
||||||
|
<MapView style={styles.map} region={mapRegion} showsUserLocation>
|
||||||
|
{caches.map((cache) => {
|
||||||
|
const found = foundCacheIds.includes(cache.CacheID);
|
||||||
|
return (
|
||||||
|
<Marker
|
||||||
|
key={cache.CacheID}
|
||||||
|
coordinate={{
|
||||||
|
latitude: cache.CacheLatitude,
|
||||||
|
longitude: cache.CacheLongitude,
|
||||||
|
}}
|
||||||
|
title={cache.CacheName}
|
||||||
|
description={`${cache.CachePoints ?? 0} pts${found ? " · Found" : ""}`}
|
||||||
|
pinColor={found ? "#22c55e" : "#ef4444"}
|
||||||
|
onCalloutPress={() => handleCacheSelect(cache)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</MapView>
|
||||||
|
) : (
|
||||||
|
<CacheList
|
||||||
|
caches={caches}
|
||||||
|
foundCacheIds={foundCacheIds}
|
||||||
|
onSelect={handleCacheSelect}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ButtonTray>
|
||||||
|
<Button label="Leaderboard" onClick={handleGotoLeaderboard} />
|
||||||
|
<Button label="Refresh" onClick={loadData} />
|
||||||
|
</ButtonTray>
|
||||||
|
</View>
|
||||||
|
</Screen>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TabButton = ({ label, active, onPress }) => (
|
||||||
|
<Text
|
||||||
|
onPress={onPress}
|
||||||
|
style={[tabStyles.tab, active && tabStyles.activeTab]}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
|
||||||
|
const tabStyles = StyleSheet.create({
|
||||||
|
tab: {
|
||||||
|
flex: 1,
|
||||||
|
textAlign: "center",
|
||||||
|
paddingVertical: 8,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#6b7280",
|
||||||
|
borderBottomWidth: 2,
|
||||||
|
borderBottomColor: "transparent",
|
||||||
|
},
|
||||||
|
activeTab: {
|
||||||
|
color: "#111827",
|
||||||
|
borderBottomColor: "#111827",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
gap: 10,
|
||||||
|
},
|
||||||
|
center: {
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
tabs: {
|
||||||
|
flexDirection: "row",
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: "#e5e7eb",
|
||||||
|
},
|
||||||
|
map: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
color: "#dc2626",
|
||||||
|
fontSize: 14,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default GlobalMapScreen;
|
||||||
Vendored
+89
@@ -0,0 +1,89 @@
|
|||||||
|
import { Pressable, StyleSheet, Text, View } from "react-native";
|
||||||
|
|
||||||
|
const CacheItem = ({ cache, isFound, onSelect }) => {
|
||||||
|
// Initialisations ---------------------
|
||||||
|
// State -------------------------------
|
||||||
|
// Handlers ----------------------------
|
||||||
|
|
||||||
|
const handleSelect = () => {
|
||||||
|
onSelect(cache);
|
||||||
|
};
|
||||||
|
|
||||||
|
// View --------------------------------
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
onPress={handleSelect}
|
||||||
|
style={({ pressed }) => [styles.item, pressed && styles.pressedItem]}
|
||||||
|
>
|
||||||
|
<View style={styles.row}>
|
||||||
|
<Text style={styles.name} numberOfLines={1}>
|
||||||
|
{cache.CacheName}
|
||||||
|
</Text>
|
||||||
|
<View
|
||||||
|
style={[styles.badge, isFound ? styles.badgeFound : styles.badgeOpen]}
|
||||||
|
>
|
||||||
|
<Text style={styles.badgeText}>{isFound ? "Found" : "Open"}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
{cache.CacheClue ? (
|
||||||
|
<Text style={styles.clue} numberOfLines={2}>
|
||||||
|
{cache.CacheClue}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
<Text style={styles.points}>Points: {cache.CachePoints ?? 0}</Text>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
item: {
|
||||||
|
paddingVertical: 14,
|
||||||
|
paddingHorizontal: 2,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderColor: "lightgray",
|
||||||
|
gap: 4,
|
||||||
|
},
|
||||||
|
pressedItem: {
|
||||||
|
backgroundColor: "#f0f9ff",
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#111827",
|
||||||
|
flex: 1,
|
||||||
|
marginRight: 8,
|
||||||
|
},
|
||||||
|
clue: {
|
||||||
|
fontSize: 13,
|
||||||
|
color: "#6b7280",
|
||||||
|
},
|
||||||
|
points: {
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: "500",
|
||||||
|
color: "#2563eb",
|
||||||
|
},
|
||||||
|
badge: {
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 2,
|
||||||
|
borderRadius: 10,
|
||||||
|
},
|
||||||
|
badgeFound: {
|
||||||
|
backgroundColor: "#d1fae5",
|
||||||
|
},
|
||||||
|
badgeOpen: {
|
||||||
|
backgroundColor: "#fef3c7",
|
||||||
|
},
|
||||||
|
badgeText: {
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#374151",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default CacheItem;
|
||||||
Vendored
+28
@@ -0,0 +1,28 @@
|
|||||||
|
import { ScrollView, StyleSheet } from "react-native";
|
||||||
|
import CacheItem from "./CacheItem";
|
||||||
|
|
||||||
|
const CacheList = ({ caches, foundCacheIds, onSelect }) => {
|
||||||
|
// Initialisations ---------------------
|
||||||
|
// State -------------------------------
|
||||||
|
// Handlers ----------------------------
|
||||||
|
// View --------------------------------
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView style={styles.container}>
|
||||||
|
{caches.map((cache) => (
|
||||||
|
<CacheItem
|
||||||
|
key={cache.CacheID}
|
||||||
|
cache={cache}
|
||||||
|
isFound={foundCacheIds.includes(cache.CacheID)}
|
||||||
|
onSelect={onSelect}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default CacheList;
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { Pressable, StyleSheet, Text, View } from "react-native";
|
||||||
|
|
||||||
|
const EventItem = ({ event, onSelect }) => {
|
||||||
|
// Initialisations ---------------------
|
||||||
|
// State -------------------------------
|
||||||
|
// Handlers ----------------------------
|
||||||
|
|
||||||
|
const handleSelect = () => {
|
||||||
|
onSelect(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dt) => {
|
||||||
|
if (!dt) return "—";
|
||||||
|
return new Date(dt).toLocaleDateString();
|
||||||
|
};
|
||||||
|
|
||||||
|
// View --------------------------------
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
onPress={handleSelect}
|
||||||
|
style={({ pressed }) => [styles.item, pressed && styles.pressedItem]}
|
||||||
|
>
|
||||||
|
<View style={styles.row}>
|
||||||
|
<Text style={styles.name} numberOfLines={1}>
|
||||||
|
{event.EventName}
|
||||||
|
</Text>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.badge,
|
||||||
|
event.EventIspublic ? styles.badgePublic : styles.badgePrivate,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text style={styles.badgeText}>
|
||||||
|
{event.EventIspublic ? "Public" : "Private"}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
{event.EventDescription ? (
|
||||||
|
<Text style={styles.description} numberOfLines={2}>
|
||||||
|
{event.EventDescription}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
<Text style={styles.meta}>
|
||||||
|
{formatDate(event.EventStart)} — {formatDate(event.EventFinish)}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
item: {
|
||||||
|
paddingVertical: 14,
|
||||||
|
paddingHorizontal: 2,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderColor: "lightgray",
|
||||||
|
gap: 4,
|
||||||
|
},
|
||||||
|
pressedItem: {
|
||||||
|
backgroundColor: "#f0f9ff",
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#111827",
|
||||||
|
flex: 1,
|
||||||
|
marginRight: 8,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
fontSize: 13,
|
||||||
|
color: "#6b7280",
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: "#9ca3af",
|
||||||
|
},
|
||||||
|
badge: {
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 2,
|
||||||
|
borderRadius: 10,
|
||||||
|
},
|
||||||
|
badgePublic: {
|
||||||
|
backgroundColor: "#dcfce7",
|
||||||
|
},
|
||||||
|
badgePrivate: {
|
||||||
|
backgroundColor: "#f3f4f6",
|
||||||
|
},
|
||||||
|
badgeText: {
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#374151",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default EventItem;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ScrollView, StyleSheet } from "react-native";
|
||||||
|
import EventItem from "./EventItem";
|
||||||
|
|
||||||
|
const EventList = ({ events, onSelect }) => {
|
||||||
|
// Initialisations ---------------------
|
||||||
|
// State -------------------------------
|
||||||
|
// Handlers ----------------------------
|
||||||
|
// View --------------------------------
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView style={styles.container}>
|
||||||
|
{events.map((event) => (
|
||||||
|
<EventItem key={event.EventID} event={event} onSelect={onSelect} />
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default EventList;
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { StyleSheet, Text, View } from "react-native";
|
||||||
|
|
||||||
|
const FindItem = ({ find }) => {
|
||||||
|
// Initialisations ---------------------
|
||||||
|
// State -------------------------------
|
||||||
|
// Handlers ----------------------------
|
||||||
|
|
||||||
|
const formatDate = (dt) => {
|
||||||
|
if (!dt) return "—";
|
||||||
|
return new Date(dt).toLocaleString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const cacheName = find.FindCache?.CacheName ?? `Cache #${find.FindCacheID}`;
|
||||||
|
const points = find.FindCache?.CachePoints ?? 0;
|
||||||
|
const username =
|
||||||
|
find.FindPlayer?.PlayerUser?.UserUsername ?? `Player #${find.FindPlayerID}`;
|
||||||
|
|
||||||
|
// View --------------------------------
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.item}>
|
||||||
|
<View style={styles.row}>
|
||||||
|
<Text style={styles.cacheName} numberOfLines={1}>
|
||||||
|
{cacheName}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.points}>+{points} pts</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.meta}>
|
||||||
|
{username} · {formatDate(find.FindDatetime)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
item: {
|
||||||
|
paddingVertical: 12,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderColor: "lightgray",
|
||||||
|
gap: 3,
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
cacheName: {
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#111827",
|
||||||
|
flex: 1,
|
||||||
|
marginRight: 8,
|
||||||
|
},
|
||||||
|
points: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: "700",
|
||||||
|
color: "#16a34a",
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: "#9ca3af",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default FindItem;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ScrollView, StyleSheet } from "react-native";
|
||||||
|
import FindItem from "./FindItem";
|
||||||
|
|
||||||
|
const FindList = ({ finds }) => {
|
||||||
|
// Initialisations ---------------------
|
||||||
|
// State -------------------------------
|
||||||
|
// Handlers ----------------------------
|
||||||
|
// View --------------------------------
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView style={styles.container}>
|
||||||
|
{finds.map((find) => (
|
||||||
|
<FindItem key={find.FindID} find={find} />
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default FindList;
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
// --- Session Store (with AsyncStorage persistence) ---
|
// --- Session Store (with AsyncStorage persistence) ---
|
||||||
|
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
|
|
||||||
const STORAGE_KEY = '@mad_session';
|
const STORAGE_KEY = "@mad_session";
|
||||||
|
|
||||||
const session = {
|
const session = {
|
||||||
currentUid: null,
|
currentUid: null,
|
||||||
@@ -13,6 +13,9 @@ const session = {
|
|||||||
isAcceptedAdmin: false,
|
isAcceptedAdmin: false,
|
||||||
isPendingAdmin: false,
|
isPendingAdmin: false,
|
||||||
teamsEnabled: false,
|
teamsEnabled: false,
|
||||||
|
// Global gameplay
|
||||||
|
currentGlobalEventId: null,
|
||||||
|
currentGlobalPlayerId: null,
|
||||||
selectedCacheId: null,
|
selectedCacheId: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -61,6 +64,18 @@ const setSelectedCache = (cacheId) => {
|
|||||||
|
|
||||||
const getSession = () => ({ ...session });
|
const getSession = () => ({ ...session });
|
||||||
|
|
||||||
|
const setGlobalSession = (eventId, playerId) => {
|
||||||
|
session.currentGlobalEventId = eventId ?? null;
|
||||||
|
session.currentGlobalPlayerId = playerId ?? null;
|
||||||
|
persistSession();
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearGlobalSession = () => {
|
||||||
|
session.currentGlobalEventId = null;
|
||||||
|
session.currentGlobalPlayerId = null;
|
||||||
|
persistSession();
|
||||||
|
};
|
||||||
|
|
||||||
const clearSession = () => {
|
const clearSession = () => {
|
||||||
session.currentUid = null;
|
session.currentUid = null;
|
||||||
session.currentGid = null;
|
session.currentGid = null;
|
||||||
@@ -68,6 +83,8 @@ const clearSession = () => {
|
|||||||
session.currentTid = null;
|
session.currentTid = null;
|
||||||
session.isBusiness = null;
|
session.isBusiness = null;
|
||||||
session.isAcceptedAdmin = false;
|
session.isAcceptedAdmin = false;
|
||||||
|
session.currentGlobalEventId = null;
|
||||||
|
session.currentGlobalPlayerId = null;
|
||||||
session.isPendingAdmin = false;
|
session.isPendingAdmin = false;
|
||||||
AsyncStorage.removeItem(STORAGE_KEY).catch(() => {});
|
AsyncStorage.removeItem(STORAGE_KEY).catch(() => {});
|
||||||
};
|
};
|
||||||
@@ -103,4 +120,16 @@ const loadSession = async () => {
|
|||||||
return { ...session };
|
return { ...session };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
setSessionUser,
|
||||||
|
setSessionGroup,
|
||||||
|
setSessionTeam,
|
||||||
|
setSessionTeamsEnabled,
|
||||||
|
getSession,
|
||||||
|
clearSession,
|
||||||
|
clearGameSession,
|
||||||
|
loadSession,
|
||||||
|
setGlobalSession,
|
||||||
|
clearGlobalSession,
|
||||||
|
};
|
||||||
export { setSessionUser, setSessionGroup, setSessionTeam, setSessionTeamsEnabled, setPendingAdmin, setSelectedCache, getSession, clearSession, clearGameSession, loadSession };
|
export { setSessionUser, setSessionGroup, setSessionTeam, setSessionTeamsEnabled, setPendingAdmin, setSelectedCache, getSession, clearSession, clearGameSession, loadSession };
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import API from "../components/API/API";
|
||||||
|
|
||||||
|
const GLOBAL_BASE = process.env.EXPO_PUBLIC_GLOBAL_API_BASE;
|
||||||
|
const GLOBAL_KEY = process.env.EXPO_PUBLIC_GLOBAL_API_KEY;
|
||||||
|
|
||||||
|
// Appends the API key as a query param to any URL
|
||||||
|
const withKey = (url) => {
|
||||||
|
const separator = url.includes("?") ? "&" : "?";
|
||||||
|
return `${url}${separator}key=${GLOBAL_KEY}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useGlobalHook = () => {
|
||||||
|
// Initialisations ---------------------
|
||||||
|
|
||||||
|
const eventsEndpoint = `${GLOBAL_BASE}/api/events`;
|
||||||
|
const playersEndpoint = `${GLOBAL_BASE}/api/players`;
|
||||||
|
const cachesEndpoint = `${GLOBAL_BASE}/api/caches`;
|
||||||
|
const findsEndpoint = `${GLOBAL_BASE}/api/finds`;
|
||||||
|
const usersEndpoint = `${GLOBAL_BASE}/api/users`;
|
||||||
|
|
||||||
|
// Handlers ----------------------------
|
||||||
|
|
||||||
|
// Events
|
||||||
|
const getPublicEvents = async () => {
|
||||||
|
const response = await API.get(withKey(eventsEndpoint));
|
||||||
|
if (!response.isSuccess) return [];
|
||||||
|
return response.result.filter((e) => e.EventIspublic);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEvent = async (eventId) => {
|
||||||
|
const response = await API.get(withKey(`${eventsEndpoint}/${eventId}`));
|
||||||
|
return response.isSuccess ? response.result : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Players
|
||||||
|
const getPlayersByEvent = async (eventId) => {
|
||||||
|
const response = await API.get(
|
||||||
|
withKey(`${playersEndpoint}/events/${eventId}`),
|
||||||
|
);
|
||||||
|
return response.isSuccess ? response.result : [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const joinEvent = async (data) => {
|
||||||
|
const response = await API.post(withKey(playersEndpoint), data);
|
||||||
|
return response.isSuccess ? response.result : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Caches
|
||||||
|
const getCachesByEvent = async (eventId) => {
|
||||||
|
const response = await API.get(
|
||||||
|
withKey(`${cachesEndpoint}/events/${eventId}`),
|
||||||
|
);
|
||||||
|
return response.isSuccess ? response.result : [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCache = async (cacheId) => {
|
||||||
|
const response = await API.get(withKey(`${cachesEndpoint}/${cacheId}`));
|
||||||
|
return response.isSuccess ? response.result : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Finds
|
||||||
|
const getFindsByEvent = async (eventId) => {
|
||||||
|
const response = await API.get(
|
||||||
|
withKey(`${findsEndpoint}/events/${eventId}`),
|
||||||
|
);
|
||||||
|
return response.isSuccess ? response.result : [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFindsByPlayer = async (playerId) => {
|
||||||
|
const response = await API.get(
|
||||||
|
withKey(`${findsEndpoint}/players/${playerId}`),
|
||||||
|
);
|
||||||
|
return response.isSuccess ? response.result : [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const logFind = async (data) => {
|
||||||
|
const response = await API.post(withKey(findsEndpoint), data);
|
||||||
|
return response.isSuccess ? response.result : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Users
|
||||||
|
const getUser = async (userId) => {
|
||||||
|
const response = await API.get(withKey(`${usersEndpoint}/${userId}`));
|
||||||
|
return response.isSuccess ? response.result : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return --------------------------------
|
||||||
|
|
||||||
|
return {
|
||||||
|
getPublicEvents,
|
||||||
|
getEvent,
|
||||||
|
getPlayersByEvent,
|
||||||
|
joinEvent,
|
||||||
|
getCachesByEvent,
|
||||||
|
getCache,
|
||||||
|
getFindsByEvent,
|
||||||
|
getFindsByPlayer,
|
||||||
|
logFind,
|
||||||
|
getUser,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useGlobalHook;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/*import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
|
||||||
|
const Component = () => {
|
||||||
|
// Initialisations ---------------------
|
||||||
|
// State -------------------------------
|
||||||
|
// Handlers ----------------------------
|
||||||
|
// View --------------------------------
|
||||||
|
return (
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
});
|
||||||
|
export default Component;
|
||||||
|
*/
|
||||||
Reference in new issue
Block a user