mirror of
https://github.com/mudabbir-ahmad/MAD-Treasure-Hunt.git
synced 2026-10-08 03:00:20 +00:00
Add new screens for game management, team management, and cache handling; implement caching logic and session management
This commit is contained in:
1 parent
8686f0e1ff
commit
17ed120ce8
70 files changed
+2172
-3892
No files matched your search
@@ -0,0 +1,38 @@
|
||||
import {Pressable, StyleSheet, Text, View} from "react-native";
|
||||
|
||||
export const Button = ({ label, icon, onClick, styleLabel, styleButton }) => {
|
||||
return (
|
||||
<Pressable onPress={onClick} style={[styles.button, styleButton]}>
|
||||
{icon ? icon : null}
|
||||
<Text style={[styles.label, styleLabel]}>{label}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
export const ButtonTray = ({ children }) => {
|
||||
return <View style={styles.buttonTray}>{children}</View>;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
minHeight: 50,
|
||||
borderWidth: 1,
|
||||
borderRadius: 7,
|
||||
borderColor: "grey",
|
||||
backgroundColor: "white",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: 3,
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
gap: 5,
|
||||
},
|
||||
label: {
|
||||
fontSize: 16,
|
||||
},
|
||||
buttonTray: {
|
||||
flexDirection: "row",
|
||||
gap: 15,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import MapView, {Circle, Marker} from 'react-native-maps';
|
||||
|
||||
const AdminCacheEditorView = ({ userLocation, caches, onAddCache, onMoveCache }) => {
|
||||
if (!userLocation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MapView
|
||||
style={{ flex: 1 }}
|
||||
initialRegion={{
|
||||
...userLocation,
|
||||
latitudeDelta: 0.01,
|
||||
longitudeDelta: 0.01,
|
||||
}}
|
||||
region={{
|
||||
...userLocation,
|
||||
latitudeDelta: 0.01,
|
||||
longitudeDelta: 0.01,
|
||||
}}
|
||||
onLongPress={(event) => onAddCache(event.nativeEvent.coordinate)}
|
||||
showsUserLocation
|
||||
>
|
||||
{(caches || []).map((cache) => (
|
||||
<React.Fragment key={cache.id}>
|
||||
<Marker
|
||||
coordinate={cache.coordinates}
|
||||
draggable
|
||||
onDragEnd={(event) => onMoveCache(cache.id, event.nativeEvent.coordinate)}
|
||||
title={`Cache ${cache.id}`}
|
||||
/>
|
||||
<Circle
|
||||
center={cache.coordinates}
|
||||
radius={cache.radius}
|
||||
fillColor="rgba(59, 130, 246, 0.15)"
|
||||
strokeColor="rgba(59, 130, 246, 0.85)"
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</MapView>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminCacheEditorView;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {StyleSheet, Text, View} from 'react-native';
|
||||
|
||||
const ClaimTimerView = ({ cache, onClaimSuccess, isClaiming }) => {
|
||||
const [timeLeft, setTimeLeft] = useState(5);
|
||||
|
||||
useEffect(() => {
|
||||
let timer;
|
||||
if (isClaiming && timeLeft > 0) {
|
||||
timer = setTimeout(() => setTimeLeft((prev) => prev - 1), 1000);
|
||||
} else if (isClaiming && timeLeft === 0 && cache) {
|
||||
onClaimSuccess(cache.id);
|
||||
} else {
|
||||
setTimeLeft(5);
|
||||
}
|
||||
return () => clearTimeout(timer);
|
||||
}, [isClaiming, timeLeft, cache, onClaimSuccess]);
|
||||
|
||||
if (!isClaiming) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.overlay}>
|
||||
<Text style={styles.text}>Hold steady. Claiming in {timeLeft}...</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
overlay: {
|
||||
position: 'absolute',
|
||||
bottom: 24,
|
||||
alignSelf: 'center',
|
||||
backgroundColor: 'rgba(0,0,0,0.75)',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 10,
|
||||
},
|
||||
text: {
|
||||
color: '#ffffff',
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
},
|
||||
});
|
||||
|
||||
export default ClaimTimerView;
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import MapView, {Circle} from 'react-native-maps';
|
||||
import ClaimTimerView from './ClaimTimerView';
|
||||
|
||||
const PlayerMapView = ({ userLocation, visibleCache, isClaiming, onClaimSuccess }) => {
|
||||
if (!userLocation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<MapView
|
||||
style={{ flex: 1 }}
|
||||
initialRegion={{
|
||||
...userLocation,
|
||||
latitudeDelta: 0.01,
|
||||
longitudeDelta: 0.01,
|
||||
}}
|
||||
region={{
|
||||
...userLocation,
|
||||
latitudeDelta: 0.01,
|
||||
longitudeDelta: 0.01,
|
||||
}}
|
||||
showsUserLocation
|
||||
>
|
||||
{visibleCache ? (
|
||||
<Circle
|
||||
center={visibleCache.coordinates}
|
||||
radius={visibleCache.radius}
|
||||
fillColor="rgba(250, 204, 21, 0.20)"
|
||||
strokeColor="rgba(250, 204, 21, 0.90)"
|
||||
/>
|
||||
) : null}
|
||||
</MapView>
|
||||
|
||||
<ClaimTimerView
|
||||
cache={visibleCache}
|
||||
isClaiming={isClaiming}
|
||||
onClaimSuccess={onClaimSuccess}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlayerMapView;
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import {Pressable, StyleSheet, Text, View} from 'react-native';
|
||||
|
||||
const BottomNavbar = ({ navigation, routeName }) => {
|
||||
const tabs = [
|
||||
{
|
||||
label: 'Map',
|
||||
route: 'MapScreen',
|
||||
onPress: () => navigation.navigate('Game', { screen: 'MapScreen' }),
|
||||
},
|
||||
{
|
||||
label: 'Leaderboard',
|
||||
route: 'LeaderboardScreen',
|
||||
onPress: () => navigation.navigate('Game', { screen: 'LeaderboardScreen' }),
|
||||
},
|
||||
{
|
||||
label: 'Current Team',
|
||||
route: 'TeamScreen',
|
||||
onPress: () => navigation.navigate('Game', { screen: 'TeamScreen' }),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{tabs.map((tab) => {
|
||||
const isActive = routeName === tab.route;
|
||||
return (
|
||||
<Pressable
|
||||
key={tab.route}
|
||||
onPress={tab.onPress}
|
||||
style={[styles.tab, isActive && styles.tabActive]}
|
||||
>
|
||||
<Text style={[styles.tabText, isActive && styles.tabTextActive]}>{tab.label}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
minHeight: 64,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#e5e7eb',
|
||||
backgroundColor: '#ffffff',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-around',
|
||||
paddingHorizontal: 8,
|
||||
},
|
||||
tab: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 12,
|
||||
borderRadius: 8,
|
||||
marginHorizontal: 4,
|
||||
},
|
||||
tabActive: {
|
||||
backgroundColor: '#eff6ff',
|
||||
},
|
||||
tabText: {
|
||||
color: '#374151',
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
},
|
||||
tabTextActive: {
|
||||
color: '#2563eb',
|
||||
},
|
||||
});
|
||||
|
||||
export default BottomNavbar;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import {StatusBar} from "expo-status-bar";
|
||||
import {StyleSheet, View} from "react-native";
|
||||
import {useNavigation, useRoute} from "@react-navigation/native";
|
||||
import TopNavbar from "./TopNavbar";
|
||||
import BottomNavbar from "./BottomNavbar";
|
||||
|
||||
const Screen = ({ children, style }) => {
|
||||
const navigation = useNavigation();
|
||||
const route = useRoute();
|
||||
|
||||
const titleMap = {
|
||||
LoginScreen: "Login",
|
||||
RegisterScreen: "Register",
|
||||
GameTypeScreen: "Game Type",
|
||||
JoinPrivateGameScreen: "Join Private Game",
|
||||
ManagePrivateGameScreen: "Manage Private Game",
|
||||
CreateGameScreen: "Create Game",
|
||||
ManageGameScreen: "Manage Game",
|
||||
GameLobbyScreen: "Leaderboard",
|
||||
LeaderboardScreen: "Leaderboard",
|
||||
TeamScreen: "Current Team",
|
||||
MapScreen: "Map",
|
||||
AdminHubScreen: "Admin Hub",
|
||||
ManageSubgroupsScreen: "Manage Subgroups",
|
||||
};
|
||||
|
||||
const routeName = route.name;
|
||||
const showBack = routeName !== "LoginScreen" && navigation.canGoBack();
|
||||
const pageTitle = titleMap[routeName] || "Treasure Hunt";
|
||||
|
||||
return (
|
||||
<View style={[styles.screen, style]}>
|
||||
<TopNavbar title={pageTitle} showBack={showBack} navigation={navigation} />
|
||||
<View style={styles.content}>{children}</View>
|
||||
<BottomNavbar navigation={navigation} routeName={routeName} />
|
||||
<StatusBar style="light" />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
screen: {
|
||||
flex: 1,
|
||||
backgroundColor: "#fff",
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
padding: 15,
|
||||
},
|
||||
});
|
||||
|
||||
export default Screen;
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import {Pressable, StyleSheet, Text, View} from 'react-native';
|
||||
import {clearSession} from '../../hooks/SessionStore';
|
||||
|
||||
const TopNavbar = ({ title, showBack, navigation }) => {
|
||||
const routeName = navigation.getState()?.routes?.[navigation.getState().index]?.name;
|
||||
const showLogout = routeName !== 'LoginScreen' && routeName !== 'RegisterScreen';
|
||||
|
||||
const handleBack = () => {
|
||||
if (navigation.canGoBack()) {
|
||||
navigation.goBack();
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
clearSession();
|
||||
navigation.reset({
|
||||
index: 0,
|
||||
routes: [{ name: 'Auth', params: { screen: 'LoginScreen' } }],
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.leftSlot}>
|
||||
{showBack ? (
|
||||
<Pressable onPress={handleBack} style={styles.iconButton}>
|
||||
<Text style={styles.iconText}>{'<'}</Text>
|
||||
</Pressable>
|
||||
) : null}
|
||||
</View>
|
||||
<Text style={styles.title}>{title}</Text>
|
||||
<View style={styles.rightSlot}>
|
||||
{showLogout ? (
|
||||
<Pressable onPress={handleLogout} style={styles.logoutButton}>
|
||||
<Text style={styles.logoutText}>Logout</Text>
|
||||
</Pressable>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
minHeight: 56,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#e5e7eb',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 12,
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
leftSlot: {
|
||||
width: 72,
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
rightSlot: {
|
||||
width: 72,
|
||||
alignItems: 'flex-end',
|
||||
},
|
||||
iconButton: {
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 10,
|
||||
},
|
||||
iconText: {
|
||||
fontSize: 20,
|
||||
color: '#111827',
|
||||
fontWeight: '700',
|
||||
},
|
||||
title: {
|
||||
flex: 1,
|
||||
textAlign: 'center',
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: '#111827',
|
||||
},
|
||||
logoutButton: {
|
||||
paddingVertical: 6,
|
||||
paddingHorizontal: 8,
|
||||
},
|
||||
logoutText: {
|
||||
color: '#2563eb',
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
export default TopNavbar;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import {StyleSheet, Text} from 'react-native';
|
||||
import Screen from '../layout/Screen';
|
||||
|
||||
const AdminHubScreen = () => (
|
||||
<Screen style={styles.container}>
|
||||
<Text style={styles.title}>Admin Hub</Text>
|
||||
<Text style={styles.body}>Admin hub setup goes here.</Text>
|
||||
</Screen>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { justifyContent: 'center', alignItems: 'center' },
|
||||
title: { fontSize: 24, fontWeight: 'bold' },
|
||||
body: { color: '#4b5563', fontSize: 15, marginTop: 10 },
|
||||
});
|
||||
|
||||
export default AdminHubScreen;
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import {StyleSheet, Text} from 'react-native';
|
||||
import Screen from '../layout/Screen';
|
||||
|
||||
const CreateGameScreen = () => (
|
||||
<Screen style={styles.container}>
|
||||
<Text style={styles.title}>Create Game</Text>
|
||||
<Text style={styles.body}>Create game setup goes here.</Text>
|
||||
</Screen>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { justifyContent: 'center', alignItems: 'center' },
|
||||
title: { fontSize: 24, fontWeight: 'bold' },
|
||||
body: { color: '#4b5563', fontSize: 15, marginTop: 10 },
|
||||
});
|
||||
|
||||
export default CreateGameScreen;
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import {StyleSheet, Text} from 'react-native';
|
||||
import Screen from '../layout/Screen';
|
||||
|
||||
const GameLobbyScreen = () => (
|
||||
<Screen style={styles.container}>
|
||||
<Text style={styles.title}>Game Lobby</Text>
|
||||
<Text style={styles.body}>Game lobby setup goes here.</Text>
|
||||
</Screen>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { justifyContent: 'center', alignItems: 'center' },
|
||||
title: { fontSize: 24, fontWeight: 'bold' },
|
||||
body: { color: '#4b5563', fontSize: 15, marginTop: 10 },
|
||||
});
|
||||
|
||||
export default GameLobbyScreen;
|
||||
@@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import {Pressable, StyleSheet, Text, View} from 'react-native';
|
||||
import useGameHook from '../../hooks/useGameHook';
|
||||
import Screen from '../layout/Screen';
|
||||
|
||||
const GameTypeScreen = ({ navigation }) => {
|
||||
const { getGameTypes } = useGameHook();
|
||||
const gameTypes = getGameTypes();
|
||||
|
||||
return (
|
||||
<Screen style={styles.container}>
|
||||
<Text style={styles.title}>Select Game Type</Text>
|
||||
<View style={styles.content}>
|
||||
{gameTypes && gameTypes.map((type) => (
|
||||
<Pressable key={type} style={styles.button} onPress={() => navigation.navigate('GameLobbyScreen')}>
|
||||
<Text style={styles.buttonText}>{type}</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
</Screen>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
content: {
|
||||
gap: 12,
|
||||
flex: 1,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
marginBottom: 20,
|
||||
},
|
||||
button: {
|
||||
backgroundColor: '#2563eb',
|
||||
borderRadius: 8,
|
||||
paddingVertical: 12,
|
||||
alignItems: 'center',
|
||||
},
|
||||
buttonText: {
|
||||
color: '#ffffff',
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
export default GameTypeScreen;
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import {StyleSheet, Text} from 'react-native';
|
||||
import Screen from '../layout/Screen';
|
||||
|
||||
const JoinPrivateGameScreen = () => {
|
||||
return (
|
||||
<Screen style={styles.container}>
|
||||
<Text style={styles.title}>Join Private Game</Text>
|
||||
<Text style={styles.body}>Join private game setup goes here.</Text>
|
||||
</Screen>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
body: {
|
||||
color: '#4b5563',
|
||||
fontSize: 15,
|
||||
marginTop: 10,
|
||||
},
|
||||
});
|
||||
|
||||
export default JoinPrivateGameScreen;
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import {StyleSheet, Text} from 'react-native';
|
||||
import Screen from '../layout/Screen';
|
||||
|
||||
const LeaderboardScreen = () => (
|
||||
<Screen style={styles.container}>
|
||||
<Text style={styles.title}>Leaderboard</Text>
|
||||
<Text style={styles.body}>Leaderboard view goes here.</Text>
|
||||
</Screen>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { justifyContent: 'center', alignItems: 'center' },
|
||||
title: { fontSize: 24, fontWeight: 'bold' },
|
||||
body: { color: '#4b5563', fontSize: 15, marginTop: 10 },
|
||||
});
|
||||
|
||||
export default LeaderboardScreen;
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import React, {useState} from 'react';
|
||||
import {Pressable, StyleSheet, Text, TextInput, View} from 'react-native';
|
||||
import useAuthHook from '../../hooks/useAuthHook';
|
||||
import Screen from '../layout/Screen';
|
||||
import {Button, ButtonTray} from '../UI/Button';
|
||||
|
||||
const LoginScreen = ({ navigation }) => {
|
||||
const { email, password, error, isLoading, setEmail, setPassword, login } = useAuthHook();
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const handleLogin = async () => {
|
||||
setSubmitting(true);
|
||||
const user = await login();
|
||||
setSubmitting(false);
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
if (user.IsAcceptedAdmin) {
|
||||
navigation.reset({ index: 0, routes: [{ name: 'Admin' }] });
|
||||
return;
|
||||
}
|
||||
navigation.reset({ index: 0, routes: [{ name: 'Game' }] });
|
||||
};
|
||||
|
||||
return (
|
||||
<Screen style={styles.container}>
|
||||
<View style={styles.content}>
|
||||
<Text style={styles.title}>Login</Text>
|
||||
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Email"
|
||||
autoCapitalize="none"
|
||||
keyboardType="email-address"
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
editable={!isLoading}
|
||||
/>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Password"
|
||||
secureTextEntry
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
editable={!isLoading}
|
||||
/>
|
||||
|
||||
{error ? <Text style={styles.errorText}>{error}</Text> : null}
|
||||
|
||||
<ButtonTray>
|
||||
<Button
|
||||
label={submitting ? 'Logging in...' : 'Login'}
|
||||
onClick={handleLogin}
|
||||
styleButton={styles.submitButton}
|
||||
styleLabel={styles.submitText}
|
||||
/>
|
||||
</ButtonTray>
|
||||
|
||||
<Pressable onPress={() => navigation.navigate('Auth', { screen: 'RegisterScreen' })}>
|
||||
<Text style={styles.linkText}>Create new account</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</Screen>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
content: {
|
||||
gap: 15,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
marginBottom: 10,
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#d1d5db',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
fontSize: 16,
|
||||
},
|
||||
errorText: {
|
||||
color: '#dc2626',
|
||||
textAlign: 'center',
|
||||
marginTop: 5,
|
||||
},
|
||||
submitButton: {
|
||||
backgroundColor: '#2563eb',
|
||||
borderColor: '#2563eb',
|
||||
},
|
||||
submitText: {
|
||||
color: '#ffffff',
|
||||
fontWeight: '600',
|
||||
},
|
||||
linkText: {
|
||||
textAlign: 'center',
|
||||
color: '#2563eb',
|
||||
fontWeight: '600',
|
||||
paddingVertical: 10,
|
||||
},
|
||||
});
|
||||
|
||||
export default LoginScreen;
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import {ActivityIndicator, StyleSheet, Text, View} from 'react-native';
|
||||
import * as Location from 'expo-location';
|
||||
import Screen from '../layout/Screen';
|
||||
import AdminCacheEditorView from '../gameplay/AdminCacheEditorView';
|
||||
import useGameHook from '../../hooks/useGameHook';
|
||||
import {getSession} from '../../hooks/SessionStore';
|
||||
|
||||
const ManageGameScreen = () => {
|
||||
const session = getSession();
|
||||
const { getCaches, upsertCache } = useGameHook();
|
||||
const [userLocation, setUserLocation] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [cacheRecords, setCacheRecords] = useState([]);
|
||||
|
||||
const loadCaches = useCallback(() => {
|
||||
if (!session.currentGid) {
|
||||
setCacheRecords([]);
|
||||
return;
|
||||
}
|
||||
setCacheRecords(getCaches(session.currentGid, null));
|
||||
}, [getCaches, session.currentGid]);
|
||||
|
||||
useEffect(() => {
|
||||
loadCaches();
|
||||
}, [loadCaches]);
|
||||
|
||||
useEffect(() => {
|
||||
let locationSub;
|
||||
|
||||
const start = async () => {
|
||||
const { status } = await Location.requestForegroundPermissionsAsync();
|
||||
if (status !== 'granted') {
|
||||
setError('Location permission denied');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const current = await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.Balanced });
|
||||
setUserLocation({ latitude: current.coords.latitude, longitude: current.coords.longitude });
|
||||
|
||||
locationSub = await Location.watchPositionAsync(
|
||||
{
|
||||
accuracy: Location.Accuracy.Balanced,
|
||||
distanceInterval: 1,
|
||||
timeInterval: 1000,
|
||||
},
|
||||
(next) => {
|
||||
setUserLocation({ latitude: next.coords.latitude, longitude: next.coords.longitude });
|
||||
},
|
||||
);
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
start();
|
||||
|
||||
return () => {
|
||||
if (locationSub) {
|
||||
locationSub.remove();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleAddCache = (coordinate) => {
|
||||
if (!session.currentGid) {
|
||||
return;
|
||||
}
|
||||
upsertCache({
|
||||
gid: session.currentGid,
|
||||
latitude: coordinate.latitude,
|
||||
longitude: coordinate.longitude,
|
||||
radius: 20,
|
||||
clue: `Cache ${cacheRecords.length + 1}`,
|
||||
subgroupId: 1,
|
||||
});
|
||||
loadCaches();
|
||||
};
|
||||
|
||||
const handleMoveCache = (cacheId, coordinate) => {
|
||||
if (!session.currentGid) {
|
||||
return;
|
||||
}
|
||||
const current = cacheRecords.find((cache) => cache.id === cacheId);
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
upsertCache({
|
||||
gid: session.currentGid,
|
||||
cacheId,
|
||||
latitude: coordinate.latitude,
|
||||
longitude: coordinate.longitude,
|
||||
radius: current.radius,
|
||||
clue: current.clue,
|
||||
subgroupId: current.subgroupId,
|
||||
});
|
||||
loadCaches();
|
||||
};
|
||||
|
||||
if (!session.currentGid) {
|
||||
return (
|
||||
<Screen style={styles.center}>
|
||||
<Text style={styles.body}>Create or join a game first.</Text>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
|
||||
if (!session.isAcceptedAdmin) {
|
||||
return (
|
||||
<Screen style={styles.center}>
|
||||
<Text style={styles.body}>Only admins can edit cache locations.</Text>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Screen style={styles.center}>
|
||||
<ActivityIndicator size="large" />
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Screen style={styles.center}>
|
||||
<Text style={styles.error}>{error}</Text>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Screen style={styles.container}>
|
||||
<View style={styles.mapWrap}>
|
||||
<AdminCacheEditorView
|
||||
userLocation={userLocation}
|
||||
caches={cacheRecords}
|
||||
onAddCache={handleAddCache}
|
||||
onMoveCache={handleMoveCache}
|
||||
/>
|
||||
</View>
|
||||
</Screen>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
padding: 0,
|
||||
},
|
||||
mapWrap: {
|
||||
flex: 1,
|
||||
},
|
||||
center: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
body: {
|
||||
color: '#4b5563',
|
||||
fontSize: 15,
|
||||
},
|
||||
error: {
|
||||
color: '#dc2626',
|
||||
fontSize: 15,
|
||||
},
|
||||
});
|
||||
|
||||
export default ManageGameScreen;
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import {StyleSheet, Text} from 'react-native';
|
||||
import Screen from '../layout/Screen';
|
||||
|
||||
const ManagePrivateGameScreen = () => {
|
||||
return (
|
||||
<Screen style={styles.container}>
|
||||
<Text style={styles.title}>Manage Private Game</Text>
|
||||
<Text style={styles.body}>Manage private game setup goes here.</Text>
|
||||
</Screen>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { justifyContent: 'center', alignItems: 'center' },
|
||||
title: { fontSize: 24, fontWeight: 'bold' },
|
||||
body: { color: '#4b5563', fontSize: 15, marginTop: 10 },
|
||||
});
|
||||
|
||||
export default ManagePrivateGameScreen;
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import {StyleSheet, Text} from 'react-native';
|
||||
import Screen from '../layout/Screen';
|
||||
|
||||
const ManageSubgroupsScreen = () => (
|
||||
<Screen style={styles.container}>
|
||||
<Text style={styles.title}>Manage Subgroups</Text>
|
||||
<Text style={styles.body}>Manage subgroups setup goes here.</Text>
|
||||
</Screen>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { justifyContent: 'center', alignItems: 'center' },
|
||||
title: { fontSize: 24, fontWeight: 'bold' },
|
||||
body: { color: '#4b5563', fontSize: 15, marginTop: 10 },
|
||||
});
|
||||
|
||||
export default ManageSubgroupsScreen;
|
||||
@@ -0,0 +1,183 @@
|
||||
import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
import {ActivityIndicator, StyleSheet, Text, View} from 'react-native';
|
||||
import * as Location from 'expo-location';
|
||||
import {Magnetometer} from 'expo-sensors';
|
||||
import Screen from '../layout/Screen';
|
||||
import PlayerMapView from '../gameplay/PlayerMapView';
|
||||
import useGameHook from '../../hooks/useGameHook';
|
||||
import usePlayerGame from '../../hooks/usePlayerGame';
|
||||
import {getSession} from '../../hooks/SessionStore';
|
||||
import Cache from '../../models/Cache';
|
||||
|
||||
const toHeading = ({ x, y }) => {
|
||||
const angle = Math.atan2(y, x) * (180 / Math.PI);
|
||||
return angle < 0 ? angle + 360 : angle;
|
||||
};
|
||||
|
||||
const MapScreen = () => {
|
||||
const session = getSession();
|
||||
const { getCaches, claimCache } = useGameHook();
|
||||
const [userLocation, setUserLocation] = useState(null);
|
||||
const [heading, setHeading] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [cacheRecords, setCacheRecords] = useState([]);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const activeCaches = useMemo(() => (
|
||||
cacheRecords.map(
|
||||
(cache) => new Cache(
|
||||
cache.id,
|
||||
cache.coordinates.latitude,
|
||||
cache.coordinates.longitude,
|
||||
cache.radius,
|
||||
cache.clue,
|
||||
cache.groupId,
|
||||
cache.subgroupId,
|
||||
),
|
||||
)
|
||||
), [cacheRecords]);
|
||||
|
||||
const subgroupFilter = session.currentSGid;
|
||||
const { visibleCache, isClaiming, setIsClaiming } = usePlayerGame(userLocation, heading, activeCaches);
|
||||
|
||||
const loadCaches = useCallback(() => {
|
||||
if (!session.currentGid) {
|
||||
setCacheRecords([]);
|
||||
return;
|
||||
}
|
||||
const rows = getCaches(session.currentGid, subgroupFilter);
|
||||
setCacheRecords(rows);
|
||||
}, [getCaches, session.currentGid, subgroupFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
loadCaches();
|
||||
}, [loadCaches]);
|
||||
|
||||
useEffect(() => {
|
||||
let locationSub;
|
||||
let headingSub;
|
||||
|
||||
const start = async () => {
|
||||
const { status } = await Location.requestForegroundPermissionsAsync();
|
||||
if (status !== 'granted') {
|
||||
setError('Location permission denied');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const current = await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.Balanced });
|
||||
setUserLocation({ latitude: current.coords.latitude, longitude: current.coords.longitude });
|
||||
|
||||
locationSub = await Location.watchPositionAsync(
|
||||
{
|
||||
accuracy: Location.Accuracy.Balanced,
|
||||
distanceInterval: 1,
|
||||
timeInterval: 1000,
|
||||
},
|
||||
(next) => {
|
||||
setUserLocation({ latitude: next.coords.latitude, longitude: next.coords.longitude });
|
||||
},
|
||||
);
|
||||
|
||||
Magnetometer.setUpdateInterval(500);
|
||||
headingSub = Magnetometer.addListener((data) => {
|
||||
setHeading(toHeading(data));
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
start();
|
||||
|
||||
return () => {
|
||||
if (locationSub) {
|
||||
locationSub.remove();
|
||||
}
|
||||
if (headingSub) {
|
||||
headingSub.remove();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleClaim = (cacheId) => {
|
||||
if (!session.currentGid) {
|
||||
return;
|
||||
}
|
||||
claimCache({
|
||||
gid: session.currentGid,
|
||||
cacheId,
|
||||
uid: session.currentUid,
|
||||
tid: session.currentTid,
|
||||
});
|
||||
setIsClaiming(false);
|
||||
loadCaches();
|
||||
};
|
||||
|
||||
if (!session.currentGid) {
|
||||
return (
|
||||
<Screen style={styles.center}>
|
||||
<Text style={styles.body}>Join or create a game to use map features.</Text>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
|
||||
if (session.isAcceptedAdmin) {
|
||||
return (
|
||||
<Screen style={styles.center}>
|
||||
<Text style={styles.body}>Admins manage caches from Manage Game.</Text>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Screen style={styles.center}>
|
||||
<ActivityIndicator size="large" />
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Screen style={styles.center}>
|
||||
<Text style={styles.error}>{error}</Text>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Screen style={styles.container}>
|
||||
<View style={styles.mapWrap}>
|
||||
<PlayerMapView
|
||||
userLocation={userLocation}
|
||||
visibleCache={visibleCache}
|
||||
isClaiming={isClaiming}
|
||||
onClaimSuccess={handleClaim}
|
||||
/>
|
||||
</View>
|
||||
</Screen>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
padding: 0,
|
||||
},
|
||||
mapWrap: {
|
||||
flex: 1,
|
||||
},
|
||||
center: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
body: {
|
||||
color: '#4b5563',
|
||||
fontSize: 15,
|
||||
},
|
||||
error: {
|
||||
color: '#dc2626',
|
||||
fontSize: 15,
|
||||
},
|
||||
});
|
||||
|
||||
export default MapScreen;
|
||||
@@ -0,0 +1,178 @@
|
||||
import React, {useState} from 'react';
|
||||
import {Pressable, StyleSheet, Text, TextInput, View} from 'react-native';
|
||||
import useAuthHook from '../../hooks/useAuthHook';
|
||||
import Screen from '../layout/Screen';
|
||||
import {Button, ButtonTray} from '../UI/Button';
|
||||
|
||||
const RegisterScreen = ({ navigation }) => {
|
||||
const {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
confirmPassword,
|
||||
accountType,
|
||||
error,
|
||||
isLoading,
|
||||
setUsername,
|
||||
setEmail,
|
||||
setPassword,
|
||||
setConfirmPassword,
|
||||
setAccountType,
|
||||
register,
|
||||
} = useAuthHook();
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const handleRegister = async () => {
|
||||
setSubmitting(true);
|
||||
const user = await register();
|
||||
setSubmitting(false);
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
if (user.IsAcceptedAdmin) {
|
||||
navigation.reset({ index: 0, routes: [{ name: 'Admin' }] });
|
||||
return;
|
||||
}
|
||||
navigation.reset({ index: 0, routes: [{ name: 'Game' }] });
|
||||
};
|
||||
|
||||
return (
|
||||
<Screen style={styles.container}>
|
||||
<View style={styles.content}>
|
||||
<Text style={styles.title}>Create Account</Text>
|
||||
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Username"
|
||||
value={username}
|
||||
onChangeText={setUsername}
|
||||
editable={!isLoading}
|
||||
/>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Email"
|
||||
autoCapitalize="none"
|
||||
keyboardType="email-address"
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
editable={!isLoading}
|
||||
/>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Password"
|
||||
secureTextEntry
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
editable={!isLoading}
|
||||
/>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Confirm Password"
|
||||
secureTextEntry
|
||||
value={confirmPassword}
|
||||
onChangeText={setConfirmPassword}
|
||||
editable={!isLoading}
|
||||
/>
|
||||
|
||||
<View style={styles.typeRow}>
|
||||
<Pressable
|
||||
style={[styles.typeButton, accountType === 'Individual' && styles.typeButtonActive]}
|
||||
onPress={() => setAccountType('Individual')}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Text style={[styles.typeText, accountType === 'Individual' && styles.typeTextActive]}>Individual</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={[styles.typeButton, accountType === 'Business/School' && styles.typeButtonActive]}
|
||||
onPress={() => setAccountType('Business/School')}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Text style={[styles.typeText, accountType === 'Business/School' && styles.typeTextActive]}>Business</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{error ? <Text style={styles.errorText}>{error}</Text> : null}
|
||||
|
||||
<ButtonTray>
|
||||
<Button
|
||||
label={submitting ? 'Creating...' : 'Create Account'}
|
||||
onClick={handleRegister}
|
||||
styleButton={styles.submitButton}
|
||||
styleLabel={styles.submitText}
|
||||
/>
|
||||
</ButtonTray>
|
||||
|
||||
<Pressable onPress={() => navigation.navigate('Auth', { screen: 'LoginScreen' })}>
|
||||
<Text style={styles.linkText}>Back to Login</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</Screen>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
content: {
|
||||
gap: 12,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
marginBottom: 10,
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#d1d5db',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
fontSize: 16,
|
||||
},
|
||||
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',
|
||||
},
|
||||
errorText: {
|
||||
color: '#dc2626',
|
||||
textAlign: 'center',
|
||||
marginTop: 5,
|
||||
},
|
||||
submitButton: {
|
||||
backgroundColor: '#2563eb',
|
||||
borderColor: '#2563eb',
|
||||
},
|
||||
submitText: {
|
||||
color: '#ffffff',
|
||||
fontWeight: '600',
|
||||
},
|
||||
linkText: {
|
||||
textAlign: 'center',
|
||||
color: '#2563eb',
|
||||
fontWeight: '600',
|
||||
paddingVertical: 10,
|
||||
},
|
||||
});
|
||||
|
||||
export default RegisterScreen;
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import {StyleSheet, Text} from 'react-native';
|
||||
import Screen from '../layout/Screen';
|
||||
|
||||
const TeamScreen = () => (
|
||||
<Screen style={styles.container}>
|
||||
<Text style={styles.title}>Team Management</Text>
|
||||
<Text style={styles.body}>Team management setup goes here.</Text>
|
||||
</Screen>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { justifyContent: 'center', alignItems: 'center' },
|
||||
title: { fontSize: 24, fontWeight: 'bold' },
|
||||
body: { color: '#4b5563', fontSize: 15, marginTop: 10 },
|
||||
});
|
||||
|
||||
export default TeamScreen;
|
||||
Reference in new issue
Block a user