Add initial JSON data files and implement core page components for game functionality

This commit is contained in:
bobbert committed 2026-03-17 21:49:44 +00:00
1 parent 81847a9188
commit ca27f8d6ed
54 files changed
+2424 -250

No files matched your search

+70
View File
@@ -0,0 +1,70 @@
import React from 'react';
import { View, Text, Pressable, StyleSheet } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import AdminFooterNavbar from '../components/AdminFooterNavbar';
import useAdminViewModel from '../ViewModel/useAdminViewModel';
const AdminHubPage = () => {
const navigation = useNavigation();
const { createGame } = useAdminViewModel();
const handleCreateGame = async () => {
await createGame({
groupName: `Admin Game ${Date.now().toString().slice(-4)}`,
businessOrSchoolName: 'New Business',
gameType: 'private',
isBusiness: true,
});
navigation.navigate('game-lobby');
};
return (
<View style={styles.wrapper}>
<View style={styles.container}>
<Text style={styles.title}>Admin Hub</Text>
<Pressable style={styles.button} onPress={handleCreateGame}>
<Text style={styles.buttonText}>Create a Game</Text>
</Pressable>
<Pressable style={styles.button} onPress={() => navigation.navigate('game-lobby')}>
<Text style={styles.buttonText}>Manage Existing Game</Text>
</Pressable>
<Pressable style={styles.button} onPress={() => navigation.navigate('manage-subgroups')}>
<Text style={styles.buttonText}>Manage Subgroups</Text>
</Pressable>
</View>
<AdminFooterNavbar />
</View>
);
};
const styles = StyleSheet.create({
wrapper: {
flex: 1,
backgroundColor: '#ffffff',
},
container: {
flex: 1,
justifyContent: 'center',
padding: 20,
gap: 12,
},
title: {
fontSize: 26,
fontWeight: '700',
color: '#111827',
marginBottom: 8,
},
button: {
backgroundColor: '#2563eb',
borderRadius: 8,
paddingVertical: 12,
alignItems: 'center',
},
buttonText: {
color: '#ffffff',
fontWeight: '600',
},
});
export default AdminHubPage;
+97 -7
View File
@@ -1,15 +1,105 @@
import React from 'react';
import { View, Text, Button, TextInput } from 'react-native';
import React, { useState } from 'react';
import { View, Text, Pressable, TextInput, StyleSheet } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import MemberFooterNavbar from '../components/MemberFooterNavbar';
import useLobbyViewModel from '../ViewModel/useLobbyViewModel';
const GameLobbyPage = () => {
const navigation = useNavigation();
const { teamCode, error, setTeamCode, createLobbyTeam, joinLobbyTeam } = useLobbyViewModel();
const [hasTeam, setHasTeam] = useState(false);
const handleCreateTeam = async () => {
const team = await createLobbyTeam({
teamName: `Team-${Date.now().toString().slice(-4)}`,
});
if (!team) {
return;
}
setHasTeam(true);
navigation.navigate('team');
};
const handleJoinTeam = async () => {
const team = await joinLobbyTeam();
if (!team) {
return;
}
setHasTeam(true);
navigation.navigate('team');
};
return (
<View>
<Text>Game Lobby</Text>
<Button title="Create Team" onPress={() => {}} />
<Text>Join Team Code:</Text>
<TextInput placeholder="Enter Team Join Code" />
<View style={styles.wrapper}>
<View style={styles.container}>
<Text style={styles.title}>Game Lobby</Text>
<TextInput
style={styles.input}
placeholder="Join Team Code"
autoCapitalize="characters"
maxLength={6}
value={teamCode}
onChangeText={setTeamCode}
/>
<Pressable style={styles.secondaryButton} onPress={handleJoinTeam}>
<Text style={styles.buttonText}>Join Team</Text>
</Pressable>
<Pressable style={styles.primaryButton} onPress={handleCreateTeam}>
<Text style={styles.buttonText}>Create Team</Text>
</Pressable>
<Pressable style={styles.secondaryButton} onPress={() => navigation.navigate('map')}>
<Text style={styles.buttonText}>Open Map</Text>
</Pressable>
{!!error && <Text style={styles.errorText}>{error}</Text>}
</View>
<MemberFooterNavbar visible={hasTeam} />
</View>
);
};
const styles = StyleSheet.create({
wrapper: {
flex: 1,
backgroundColor: '#ffffff',
},
container: {
flex: 1,
justifyContent: 'center',
padding: 20,
gap: 12,
},
title: {
fontSize: 26,
fontWeight: '700',
color: '#111827',
marginBottom: 8,
},
input: {
borderWidth: 1,
borderColor: '#d1d5db',
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 10,
},
primaryButton: {
backgroundColor: '#2563eb',
borderRadius: 8,
paddingVertical: 12,
alignItems: 'center',
},
secondaryButton: {
backgroundColor: '#4b5563',
borderRadius: 8,
paddingVertical: 12,
alignItems: 'center',
},
buttonText: {
color: '#ffffff',
fontWeight: '600',
},
errorText: {
color: '#dc2626',
},
});
export default GameLobbyPage;
+51 -5
View File
@@ -1,14 +1,60 @@
import React from 'react';
import { View, Button } from 'react-native';
import { View, Text, Pressable, StyleSheet } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import useGameTypeViewModel from '../ViewModel/useGameTypeViewModel';
const GameTypePage = () => {
const navigation = useNavigation();
const { gameTypes } = useGameTypeViewModel();
const navigateByType = (label) => {
if (label === 'Join Global Game') {
navigation.navigate('game-lobby');
return;
}
if (label === 'Join Private Game') {
navigation.navigate('join-private-game');
return;
}
navigation.navigate('manage-private-subtask');
};
return (
<View>
<Button title="Join Global Game" onPress={() => {}} />
<Button title="Join Private Game" onPress={() => {}} />
<Button title="Manage Private Game" onPress={() => {}} />
<View style={styles.container}>
<Text style={styles.title}>Select Game Type</Text>
{gameTypes.map((label) => (
<Pressable key={label} style={styles.button} onPress={() => navigateByType(label)}>
<Text style={styles.buttonText}>{label}</Text>
</Pressable>
))}
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
padding: 20,
gap: 12,
backgroundColor: '#ffffff',
},
title: {
fontSize: 26,
fontWeight: '700',
color: '#111827',
marginBottom: 8,
},
button: {
backgroundColor: '#2563eb',
borderRadius: 8,
paddingVertical: 12,
alignItems: 'center',
},
buttonText: {
color: '#ffffff',
fontWeight: '600',
},
});
export default GameTypePage;
+64 -7
View File
@@ -1,19 +1,76 @@
import React, { useState } from 'react';
import { View, TextInput, Button } from 'react-native';
import { View, Text, TextInput, Pressable, StyleSheet } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import usePrivateGameViewModel from '../ViewModel/usePrivateGameViewModel';
const JoinPrivateGamePage = () => {
const [joinCode, setJoinCode] = useState('');
const navigation = useNavigation();
const { joinCode, error, setJoinCode, joinWithCode } = usePrivateGameViewModel();
const [submitting, setSubmitting] = useState(false);
const handleJoinGame = () => {
// Logic to check join code and redirect to game lobby
const handleJoinGame = async () => {
setSubmitting(true);
const group = await joinWithCode();
setSubmitting(false);
if (!group) {
return;
}
navigation.navigate('game-lobby');
};
return (
<View>
<TextInput placeholder="Enter Join Code" value={joinCode} onChangeText={setJoinCode} />
<Button title="Join Game" onPress={handleJoinGame} />
<View style={styles.container}>
<Text style={styles.title}>Join Private Game</Text>
<TextInput
style={styles.input}
placeholder="Enter Join Code"
autoCapitalize="characters"
maxLength={6}
value={joinCode}
onChangeText={setJoinCode}
/>
<Pressable style={styles.button} onPress={handleJoinGame}>
<Text style={styles.buttonText}>{submitting ? 'Joining...' : 'Join Game'}</Text>
</Pressable>
{!!error && <Text style={styles.errorText}>{error}</Text>}
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
padding: 20,
gap: 12,
backgroundColor: '#ffffff',
},
title: {
fontSize: 26,
fontWeight: '700',
color: '#111827',
marginBottom: 8,
},
input: {
borderWidth: 1,
borderColor: '#d1d5db',
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 10,
},
button: {
backgroundColor: '#2563eb',
borderRadius: 8,
paddingVertical: 12,
alignItems: 'center',
},
buttonText: {
color: '#ffffff',
fontWeight: '600',
},
errorText: {
color: '#dc2626',
},
});
export default JoinPrivateGamePage;
+114
View File
@@ -0,0 +1,114 @@
import React, { useState } from 'react';
import { View, Text, TextInput, Pressable, StyleSheet } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import useAuthViewModel from '../ViewModel/useAuthViewModel';
const LoginPage = () => {
const navigation = useNavigation();
const {
email,
password,
error,
setEmail,
setPassword,
login,
} = useAuthViewModel();
const [submitting, setSubmitting] = useState(false);
const handleLogin = async () => {
setSubmitting(true);
const user = await login();
setSubmitting(false);
if (!user) {
return;
}
if (user.SGid === 0) {
navigation.navigate('admin-hub');
return;
}
navigation.navigate('game-type');
};
return (
<View style={styles.container}>
<Text style={styles.title}>Login</Text>
<TextInput
style={styles.input}
placeholder="Email"
autoCapitalize="none"
keyboardType="email-address"
value={email}
onChangeText={setEmail}
/>
<TextInput
style={styles.input}
placeholder="Password"
secureTextEntry
value={password}
onChangeText={setPassword}
/>
<View style={styles.row}>
<Pressable style={styles.leftButton} onPress={() => navigation.navigate('register')}>
<Text style={styles.buttonText}>Register</Text>
</Pressable>
<Pressable style={styles.rightButton} onPress={handleLogin}>
<Text style={styles.buttonText}>{submitting ? 'Logging in...' : 'Login'}</Text>
</Pressable>
</View>
{!!error && <Text style={styles.errorText}>{error}</Text>}
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
justifyContent: 'center',
gap: 12,
backgroundColor: '#ffffff',
},
title: {
fontSize: 28,
fontWeight: '700',
marginBottom: 8,
color: '#111827',
},
input: {
borderWidth: 1,
borderColor: '#d1d5db',
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 10,
},
row: {
flexDirection: 'row',
gap: 10,
marginTop: 4,
},
leftButton: {
flex: 1,
backgroundColor: '#4b5563',
borderRadius: 8,
alignItems: 'center',
paddingVertical: 12,
},
rightButton: {
flex: 1,
backgroundColor: '#2563eb',
borderRadius: 8,
alignItems: 'center',
paddingVertical: 12,
},
buttonText: {
color: '#ffffff',
fontWeight: '600',
},
errorText: {
color: '#dc2626',
marginTop: 4,
},
});
export default LoginPage;
+100
View File
@@ -0,0 +1,100 @@
import React, { useState } from 'react';
import { View, Text, TextInput, Pressable, StyleSheet } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import useManagePrivateSubtaskViewModel from '../ViewModel/useManagePrivateSubtaskViewModel';
const ManagePrivateSubtaskPage = () => {
const navigation = useNavigation();
const {
businessOrSchoolName,
error,
setBusinessOrSchoolName,
createNewPrivateGame,
joinAdminForPrivate,
} = useManagePrivateSubtaskViewModel();
const [submitting, setSubmitting] = useState(false);
const handleCreate = async () => {
setSubmitting(true);
const group = await createNewPrivateGame();
setSubmitting(false);
if (!group) {
return;
}
navigation.navigate('admin-hub');
};
const handleJoinAdmin = async () => {
setSubmitting(true);
const response = await joinAdminForPrivate();
setSubmitting(false);
if (!response) {
return;
}
navigation.navigate('admin-hub');
};
return (
<View style={styles.container}>
<Text style={styles.title}>Manage Private Game</Text>
<TextInput
style={styles.input}
placeholder="Business/School Name"
value={businessOrSchoolName}
onChangeText={setBusinessOrSchoolName}
/>
<Pressable style={styles.primaryButton} onPress={handleCreate}>
<Text style={styles.buttonText}>{submitting ? 'Please wait...' : 'Create New Private Game'}</Text>
</Pressable>
<Pressable style={styles.secondaryButton} onPress={handleJoinAdmin}>
<Text style={styles.buttonText}>Join as Admin for Private Game</Text>
</Pressable>
{!!error && <Text style={styles.errorText}>{error}</Text>}
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
gap: 12,
padding: 20,
backgroundColor: '#ffffff',
},
title: {
fontSize: 26,
fontWeight: '700',
color: '#111827',
marginBottom: 8,
},
input: {
borderWidth: 1,
borderColor: '#d1d5db',
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 10,
},
primaryButton: {
backgroundColor: '#2563eb',
borderRadius: 8,
alignItems: 'center',
paddingVertical: 12,
},
secondaryButton: {
backgroundColor: '#4b5563',
borderRadius: 8,
alignItems: 'center',
paddingVertical: 12,
},
buttonText: {
color: '#ffffff',
fontWeight: '600',
},
errorText: {
color: '#dc2626',
},
});
export default ManagePrivateSubtaskPage;
+118
View File
@@ -0,0 +1,118 @@
import React from 'react';
import { View, Text, TextInput, Pressable, StyleSheet } from 'react-native';
import useSubgroupAdminViewModel from '../ViewModel/useSubgroupAdminViewModel';
import AdminFooterNavbar from '../components/AdminFooterNavbar';
const ManageSubgroupsPage = () => {
const {
subgroups,
subgroupName,
cacheTriggerMeters,
error,
setSubgroupName,
setCacheTriggerMeters,
addSubgroup,
updateCacheForSubgroup,
} = useSubgroupAdminViewModel();
return (
<View style={styles.wrapper}>
<View style={styles.container}>
<Text style={styles.title}>Manage Subgroups</Text>
{subgroups.map((subgroup) => (
<View key={`${subgroup.Gid}_${subgroup.SGid}`} style={styles.row}>
<Text style={styles.rowText}>
SGid {subgroup.SGid} | {subgroup.SubGroupName} | Code: {subgroup.JoinCode || 'ADMIN'} | Trigger: {subgroup.CacheTriggerMeters}m
</Text>
<Pressable
style={styles.inlineButton}
onPress={() => updateCacheForSubgroup(subgroup.SGid, (subgroup.CacheTriggerMeters || 20) + 5)}
>
<Text style={styles.inlineButtonText}>+5m</Text>
</Pressable>
</View>
))}
<TextInput
style={styles.input}
placeholder="New subgroup name"
value={subgroupName}
onChangeText={setSubgroupName}
/>
<TextInput
style={styles.input}
placeholder="Cache trigger meters"
keyboardType="numeric"
value={cacheTriggerMeters}
onChangeText={setCacheTriggerMeters}
/>
<Pressable style={styles.button} onPress={addSubgroup}>
<Text style={styles.buttonText}>Create Subgroup</Text>
</Pressable>
{!!error && <Text style={styles.errorText}>{error}</Text>}
</View>
<AdminFooterNavbar />
</View>
);
};
const styles = StyleSheet.create({
wrapper: {
flex: 1,
backgroundColor: '#ffffff',
},
container: {
flex: 1,
gap: 10,
padding: 20,
},
title: {
fontSize: 24,
fontWeight: '700',
color: '#111827',
},
row: {
borderWidth: 1,
borderColor: '#d1d5db',
borderRadius: 8,
padding: 10,
gap: 8,
},
rowText: {
color: '#111827',
fontSize: 13,
},
inlineButton: {
alignSelf: 'flex-start',
backgroundColor: '#2563eb',
paddingHorizontal: 10,
paddingVertical: 6,
borderRadius: 6,
},
inlineButtonText: {
color: '#ffffff',
fontWeight: '600',
},
input: {
borderWidth: 1,
borderColor: '#d1d5db',
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 10,
},
button: {
backgroundColor: '#2563eb',
borderRadius: 8,
alignItems: 'center',
paddingVertical: 12,
},
buttonText: {
color: '#ffffff',
fontWeight: '600',
},
errorText: {
color: '#dc2626',
},
});
export default ManageSubgroupsPage;
+44
View File
@@ -0,0 +1,44 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import MemberFooterNavbar from '../components/MemberFooterNavbar';
import useMapViewModel from '../ViewModel/useMapViewModel';
const MapPage = () => {
const { points } = useMapViewModel();
return (
<View style={styles.wrapper}>
<View style={styles.container}>
<Text style={styles.title}>Map Page</Text>
<Text style={styles.body}>Map and hunt markers appear here.</Text>
<Text style={styles.body}>Points loaded: {points.length}</Text>
</View>
<MemberFooterNavbar visible />
</View>
);
};
const styles = StyleSheet.create({
wrapper: {
flex: 1,
backgroundColor: '#ffffff',
},
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
gap: 6,
},
title: {
fontSize: 26,
fontWeight: '700',
color: '#111827',
},
body: {
color: '#4b5563',
},
});
export default MapPage;
+132 -19
View File
@@ -1,31 +1,144 @@
import React, { useState } from 'react';
import { View, Text, TextInput, Button, Picker } from 'react-native';
import { View, Text, TextInput, Pressable, StyleSheet } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import useAuthViewModel from '../ViewModel/useAuthViewModel';
const RegisterPage = () => {
const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [userType, setUserType] = useState('Individual');
const navigation = useNavigation();
const {
username,
email,
password,
confirmPassword,
accountType,
error,
setUsername,
setEmail,
setPassword,
setConfirmPassword,
setAccountType,
register,
} = useAuthViewModel();
const [submitting, setSubmitting] = useState(false);
const handleRegister = () => {
// Registration logic here
const handleRegister = async () => {
setSubmitting(true);
const user = await register();
setSubmitting(false);
if (!user) {
return;
}
navigation.navigate(user.SGid === 0 ? 'admin-hub' : 'game-type');
};
return (
<View>
<Text>Register</Text>
<TextInput placeholder="Username" value={username} onChangeText={setUsername} />
<TextInput placeholder="Email" value={email} onChangeText={setEmail} />
<TextInput placeholder="Password" value={password} onChangeText={setPassword} secureTextEntry />
<TextInput placeholder="Confirm Password" value={confirmPassword} onChangeText={setConfirmPassword} secureTextEntry />
<Picker selectedValue={userType} onValueChange={(itemValue) => setUserType(itemValue)}>
<Picker.Item label="Individual" value="Individual" />
<Picker.Item label="Business/School" value="Business/School" />
</Picker>
<Button title="Register" onPress={handleRegister} />
<View style={styles.container}>
<Text style={styles.title}>Register</Text>
<TextInput style={styles.input} placeholder="Username" value={username} onChangeText={setUsername} />
<TextInput
style={styles.input}
placeholder="Email"
autoCapitalize="none"
keyboardType="email-address"
value={email}
onChangeText={setEmail}
/>
<TextInput style={styles.input} placeholder="Password" value={password} onChangeText={setPassword} secureTextEntry />
<TextInput
style={styles.input}
placeholder="Confirm Password"
value={confirmPassword}
onChangeText={setConfirmPassword}
secureTextEntry
/>
<View style={styles.typeRow}>
<Pressable
style={[styles.typeButton, accountType === 'Individual' && styles.typeButtonActive]}
onPress={() => setAccountType('Individual')}
>
<Text style={[styles.typeText, accountType === 'Individual' && styles.typeTextActive]}>Individual</Text>
</Pressable>
<Pressable
style={[styles.typeButton, accountType === 'Business/School' && styles.typeButtonActive]}
onPress={() => setAccountType('Business/School')}
>
<Text style={[styles.typeText, accountType === 'Business/School' && styles.typeTextActive]}>Business/School</Text>
</Pressable>
</View>
<Pressable style={styles.submitButton} onPress={handleRegister}>
<Text style={styles.submitText}>{submitting ? 'Creating...' : 'Create Account'}</Text>
</Pressable>
<Pressable onPress={() => navigation.navigate('login')}>
<Text style={styles.backText}>Back to Login</Text>
</Pressable>
{!!error && <Text style={styles.errorText}>{error}</Text>}
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
justifyContent: 'center',
gap: 12,
backgroundColor: '#ffffff',
},
title: {
fontSize: 28,
fontWeight: '700',
color: '#111827',
marginBottom: 8,
},
input: {
borderWidth: 1,
borderColor: '#d1d5db',
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 10,
},
typeRow: {
flexDirection: 'row',
gap: 10,
},
typeButton: {
flex: 1,
borderWidth: 1,
borderColor: '#cbd5e1',
borderRadius: 8,
alignItems: 'center',
paddingVertical: 10,
},
typeButtonActive: {
backgroundColor: '#2563eb',
borderColor: '#2563eb',
},
typeText: {
color: '#1f2937',
fontWeight: '600',
},
typeTextActive: {
color: '#ffffff',
},
submitButton: {
backgroundColor: '#2563eb',
borderRadius: 8,
alignItems: 'center',
paddingVertical: 12,
},
submitText: {
color: '#ffffff',
fontWeight: '600',
},
backText: {
textAlign: 'center',
color: '#2563eb',
fontWeight: '600',
},
errorText: {
textAlign: 'center',
color: '#dc2626',
},
});
export default RegisterPage;
+44
View File
@@ -0,0 +1,44 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import MemberFooterNavbar from '../components/MemberFooterNavbar';
import useTeamViewModel from '../ViewModel/useTeamViewModel';
const TeamPage = () => {
const { team } = useTeamViewModel();
return (
<View style={styles.wrapper}>
<View style={styles.container}>
<Text style={styles.title}>Team Page</Text>
<Text style={styles.body}>Team details and members appear here.</Text>
<Text style={styles.body}>{team?.team?.TeamName || 'No team joined yet'}</Text>
</View>
<MemberFooterNavbar visible />
</View>
);
};
const styles = StyleSheet.create({
wrapper: {
flex: 1,
backgroundColor: '#ffffff',
},
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
gap: 6,
},
title: {
fontSize: 26,
fontWeight: '700',
color: '#111827',
},
body: {
color: '#4b5563',
},
});
export default TeamPage;