Skip to content
Snippets Groups Projects
Commit 0bd0b7b2 authored by FERNANDES SAMUEL's avatar FERNANDES SAMUEL :star:
Browse files

v3.2

parents 0e8e0fc7 66c2760e
Branches main
No related merge requests found
Showing
with 1129 additions and 251 deletions
......@@ -12,7 +12,7 @@ build:
artifacts:
paths:
- builds.zip
expire_in: 1h
expire_in: 1h
only:
- release
......@@ -34,3 +34,15 @@ deploy-backend:
- ssh -i ~/.ssh/id_rsa $SSH_USER@$VM_IPADDRESS "/usr/bin/bash -c 'sudo docker compose -f ~/the-sevens/docker-compose.yml up -d --build --remove-orphans --build || true'"
only:
- release
# Pour les builds mobiles si nécessaire
deploy-mobile:
stage: deploy
script:
- cd QuizzMobile/
- yarn install
- apk add --no-cache bash
- yarn eas-cli build --platform android --non-interactive --no-wait
only:
- release
allow_failure: true
# LICENCES
Le code source de ce dépôt est publié sous [licence MIT](#licence-mit).
Sauf mention de propriété intellectuelle détenue par des tiers (notamment
un crédit sur certaines images), les contenus de ce dépôt sont publiés sous [licence Ouverte 2.0](#licence-ouverte-20open-licence-20).
La marque d'État est à usage exclusif des acteurs de la sphère
étatique. En particulier, la typographie Marianne© est protégée par
le droit d'auteur. [Lire les explications sur le site de la marque
d'État.](https://www.gouvernement.fr/charte/charte-graphique-les-fondamentaux/la-typographie)
## LICENCE MIT
Copyright (c) 2021 DINUM
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## LICENCE OUVERTE 2.0/OPEN LICENCE 2.0
### Réutilisation de l’« Information » sous cette licence
Le « Concédant » concède au « Réutilisateur » un droit non exclusif et gratuit de libre « Réutilisation » de l’« Information » objet de la présente licence, à des fins commerciales ou non, dans le monde entier et pour une durée illimitée, dans les conditions exprimées ci-dessous.
**Le « Réutilisateur » est libre de réutiliser l’« Information » :**
- de la communiquer, la reproduire, la copier ;
- de l’adapter, la modifier, l’extraire et la transformer, notamment pour créer des « Informations dérivées » ;
- de la diffuser, la redistribuer, la publier et la transmettre, de l’exploiter à titre commercial, par exemple en la combinant avec d’autres informations, ou en l’incluant dans votre propre produit ou application.
**Sous réserve de :**
- mentionner la paternité de l’«Information» : sa source (a minima le nom du « Concédant ») et la date de la dernière mise à jour de l’« Information » réutilisée.
Le « Réutilisateur » peut notamment s’ acquitter de cette condition en indiquant l’adresse (URL) renvoyant vers « l’Information » et assurant une mention effective de sa paternité.
**Par exemple :**
Dans le cas d’une réutilisation de la base SIRENE de l’INSEE, mentionner l’URL du « Concédant » : www.insee.fr + la date de dernière mise à jour de l’Information réutilisée.
Cette mention de paternité ne doit ni conférer un caractère officiel à la « Réutilisation » de l’« Information », ni suggérer une quelconque reconnaissance ou caution par le « Concédant », ou par toute autre entité publique, du « Réutilisateur » ou de sa « Réutilisation ».
### Données à caractère personnel
L’« Information » mise à disposition peut contenir des « Données à caractère personnel » pouvant faire l’objet d’une « Réutilisation ». Alors, le « Concédant » informe le « Réutilisateur » (par tous moyens) de leur présence, l’ « Information » peut être librement réutilisée, sans faire obstacle aux libertés accordées par la présente licence, à condition de respecter le cadre légal relatif à la protection des données à caractère personnel.
### Droits de propriété intellectuelle
Il est garanti au « Réutilisateur » que l’ « Information » ne contient pas de « Droits de propriété intellectuelle » appartenant à des tiers qui pourraient faire obstacle aux libertés qui lui sont accordées par la présente licence.
Les éventuels « Droits de propriété intellectuelle » détenus par le « Concédant » sur l’ « Information » ne font pas obstacle aux libertés qui sont accordées par la présente licence. Lorsque le « Concédant » détient des « Droits de propriété intellectuelle » » sur l’ « Information », il les cède au « Réutilisateur » de façon non exclusive, à titre gracieux, pour le monde entier, pour toute la durée des « Droits de propriété intellectuelle », et le « Réutilisateur » peut en faire tout usage conformément aux libertés et aux conditions définies par la présente licence.
### Responsabilité
L’ «Information» est mise à disposition telle que produite ou reçue, sans autre garantie expresse ou tacite qui n’est pas prévue par la présente licence. L’absence de défauts ou d’erreurs éventuellement contenues dans l’ «Information», comme la fourniture continue de l’ « Information » n’est pas garantie par le «Concédant». Il ne peut être tenu pour responsable de toute perte, préjudice ou dommage de quelque sorte causé à des tiers du fait de la « Réutilisation ».
Le « Réutilisateur » est seul responsable de la « Réutilisation » de l’« Information ».
La « Réutilisation » ne doit pas induire en erreur des tiers quant au contenu de l’« Information », sa source et sa date de mise à jour.
### Droit applicable
La présente licence est régie par le droit français.
#### Compatibilité de la présente licence
Elle a été conçue pour être compatible avec toute licence libre qui exige _a minima_ la mention de paternité. Elle est notamment compatible avec la version antérieure de la présente licence ainsi qu’avec les licences « Open Government Licence » (OGL) du Royaume-Uni, « Creative Commons Attribution » (CC-BY) de Creative Commons et « Open Data Commons Attribution » (ODC-BY) de l’Open Knowledge Foundation.
### Définitions
Sont considérés, au sens de la présente licence comme :
- Le « **Concédant** » : toute personne concédant un droit de « Réutilisation » sur l’« Information » dans les libertés et les conditions prévues par la présente licence.
- L’« **Information** » :
- toute information publique figurant dans des documents communiqués ou publiés par une administration mentionnée au premier alinéa de l’article L.300-2 du CRPA ;
- toute information mise à disposition par toute personne selon les termes et conditions de la présente licence.
- La « **Réutilisation** » : l’utilisation de l’« Information » à d’autres fins que celles pour lesquelles elle a été produite ou reçue.
- Le « **Réutilisateur** » : toute personne qui réutilise les « Informations » conformément aux conditions de la présente licence.
- Des « **Données à caractère personnel** » : toute information se rapportant à une personne physique identifiée ou identifiable, pouvant être identifiée directement ou indirectement. Leur « Réutilisation » est subordonnée au respect du cadre juridique en vigueur.
- Une « **Information dérivée** » : toute nouvelle donnée ou information créées directement à partir de l’« Information » ou à partir d’une combinaison de l’ « Information » et d’autres données ou informations non soumises à cette licence.
- Les « **Droits de propriété intellectuelle** » : tous droits identifiés comme tels par le Code de la propriété intellectuelle (droit d’auteur, droits voisins au droit d’auteur, droit sui generis des producteurs de bases de données).
### À propos de cette licence
La présente licence a vocation à être utilisée par les administrations pour la réutilisation de leurs informations publiques. Elle peut également être utilisée par toute personne souhaitant mettre à disposition de l’« Information » dans les conditions définies par la présente licence
La France est dotée d’un cadre juridique global visant à une diffusion spontanée par les administrations de leurs informations publiques afin d’en permettre la plus large réutilisation.
Le droit de la « Réutilisation » de l’« Information » des administrations est régi par le code des relations entre le public et l’administration (CRPA) et, le cas échéant, le code du patrimoine (livre II relatif aux archives).
Cette licence facilite la réutilisation libre et gratuite des informations publiques et figure parmi les licences qui peuvent être utilisées par l’administration en vertu du décret pris en application de l’article L.323-2 du CRPA.
Etalab est la mission chargée, sous l’autorité du Premier ministre, d’ouvrir le plus grand nombre de données publiques des administrations de l’État et de ses établissements publics. Elle a réalisé la Licence Ouverte pour faciliter la réutilisation libre et gratuite de ces informations publiques, telles que définies par l’article L321-1 du CRPA.
Cette licence est une version 2.0 de la Licence Ouverte.
Etalab se réserve la faculté de proposer de nouvelles versions de la Licence Ouverte. Cependant, les « Réutilisateurs » pourront continuer à réutiliser les informations disponibles sous cette licence s’ils le souhaitent.
import React, { useMemo, useReducer } from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { createDrawerNavigator } from '@react-navigation/drawer';
import { AuthContext } from './app/Components/context';
// Import des écrans
import SplashScreen from './app/Screens/SplashScreen';
import SignInScreen from './app/Screens/SignInScreen';
import SignUpScreen from './app/Screens/SignUpScreen';
import CategorySelector from './app/Screens/CategorySelector';
import QuizSettingsScreen from './app/Screens/QuizSettingsScreen';
import QuizScreen from './app/Screens/QuizScreen';
import QuizResultScreen from './app/Screens/QuizResultScreen';
import MyCreatedQuizzes from './app/Screens/MyCreatedQuizzes';
import MyPlayedQuizzes from './app/Screens/MyPlayedQuizzes';
import PublicQuizzes from './app/Screens/PublicQuizzes';
const Stack = createStackNavigator();
const Drawer = createDrawerNavigator();
// Drawer Navigator
function DrawerNavigator() {
return (
<Drawer.Navigator
initialRouteName="CategorySelector"
screenOptions={{
headerShown: true,
headerStyle: { backgroundColor: '#00C3FF' },
headerTintColor: '#fff',
headerTitleAlign: 'center',
drawerStyle: {
backgroundColor: '#f7f7f7',
},
drawerActiveTintColor: '#00C3FF',
drawerInactiveTintColor: '#666',
drawerActiveBackgroundColor: '#e0f7ff',
}}
>
<Drawer.Screen
name="CategorySelector"
component={CategorySelector}
options={{ title: 'Home' }}
/>
<Drawer.Screen
name="My Created Quizzes"
component={MyCreatedQuizzes}
options={{ title: 'My Created Quizzes', headerShown: false }}
/>
<Drawer.Screen
name="My Played Quizzes"
component={MyPlayedQuizzes}
options={{ title: 'My Played Quizzes', headerShown: false }}
/>
<Drawer.Screen
name="PublicQuizzes"
component={PublicQuizzes}
options={{ title: 'Public Quizzes', headerShown: false }}
/>
</Drawer.Navigator>
);
}
// App Component
export default function App() {
const initialLoginState = {
userToken: null,
};
const loginReducer = (prevState, action) => {
switch (action.type) {
case 'LOGIN':
return {
...prevState,
userToken: action.token,
};
case 'LOGOUT':
return {
...prevState,
userToken: null,
};
case 'REGISTER':
return {
...prevState,
userToken: action.token,
};
}
};
const [loginState, dispatch] = useReducer(loginReducer, initialLoginState);
const authContext = useMemo(
() => ({
signIn: async (email, password) => {
let userToken = null;
if (email === 'testuser@email.com' && password === 'testpass') {
userToken = 'testtoken';
}
dispatch({ type: 'LOGIN', token: userToken });
},
signOut: () => dispatch({ type: 'LOGOUT' }),
signUp: async () => dispatch({ type: 'REGISTER', token: 'newUserToken' }),
}),
[]
);
return (
<AuthContext.Provider value={authContext}>
<NavigationContainer>
<Stack.Navigator initialRouteName="SplashScreen">
<Stack.Screen
name="SplashScreen"
component={SplashScreen}
options={{ headerShown: false }}
/>
<Stack.Screen
name="SignIn"
component={SignInScreen}
options={{ headerShown: false }}
/>
<Stack.Screen
name="SignUp"
component={SignUpScreen}
options={{ headerShown: false }}
/>
<Stack.Screen
name="MainDrawer"
component={DrawerNavigator}
options={{ headerShown: false }}
/>
<Stack.Screen
name="QuizSettingsScreen"
component={QuizSettingsScreen}
options={{ headerShown: false }}
/>
<Stack.Screen
name="QuizScreen"
component={QuizScreen}
options={{
headerShown: false,
headerStyle: { backgroundColor: '#00C3FF' },
headerTintColor: '#fff',
title: 'Quiz Details',
}}
/>
<Stack.Screen
name="QuizResultScreen"
component={QuizResultScreen}
options={{ headerShown: false }}
/>
</Stack.Navigator>
</NavigationContainer>
</AuthContext.Provider>
);
}
File moved
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/QuizzMobile.iml" filepath="$PROJECT_DIR$/.idea/QuizzMobile.iml" />
</modules>
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>
\ No newline at end of file
import React, { useMemo, useReducer, useEffect } from "react";
import { NavigationContainer } from "@react-navigation/native";
import { createStackNavigator } from "@react-navigation/stack";
import { createDrawerNavigator } from "@react-navigation/drawer";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { AuthContext } from "./app/Components/context";
// Import des écrans
import SplashScreen from './app/Screens/SplashScreen';
import SignInScreen from './app/Screens/SignInScreen';
import SignUpScreen from './app/Screens/SignUpScreen';
import CategorySelector from './app/Screens/CategorySelector';
import QuizSettingsScreen from './app/Screens/QuizSettingsScreen';
import QuizScreen from './app/Screens/QuizScreen';
import QuizResultScreen from './app/Screens/QuizResultScreen';
import MyCreatedQuizzes from './app/Screens/MyCreatedQuizzes';
import MyPlayedQuizzes from './app/Screens/MyPlayedQuizzes';
import PublicQuizzes from './app/Screens/PublicQuizzes';
import GameMode from './app/Screens/GameMode';
import PasswordChangedScreen from './app/Screens/PasswordChangedScreen';
import ResetPasswordScreen from './app/Screens/ResetPasswordScreen';
import VerifyCodeScreen from './app/Screens/VerifyCodeScreen';
import ForgotPasswordScreen from './app/Screens/ForgotPasswordScreen';
import ScrumModeScreen from './app/Screens/ScrumModeScreen';
import TeamScreen from './app/Screens/TeamScreen';
const Stack = createStackNavigator();
const Drawer = createDrawerNavigator();
function DrawerNavigator() {
return (
<Drawer.Navigator
initialRouteName="CategorySelector"
screenOptions={{
headerShown: true,
headerStyle: { backgroundColor: '#00C3FF' },
headerTintColor: '#fff',
headerTitleAlign: 'center',
drawerStyle: {
backgroundColor: '#f7f7f7',
},
drawerActiveTintColor: '#00C3FF',
drawerInactiveTintColor: '#666',
drawerActiveBackgroundColor: '#e0f7ff',
}}
>
<Drawer.Screen
name="CategorySelector"
component={CategorySelector}
options={{ title: 'Home' }}
/>
<Drawer.Screen
name="My Created Quizzes"
component={MyCreatedQuizzes}
options={{ title: 'My Created Quizzes', headerShown: false }}
/>
<Drawer.Screen
name="My Played Quizzes"
component={MyPlayedQuizzes}
options={{ title: 'My Played Quizzes', headerShown: false }}
/>
<Drawer.Screen
name="PublicQuizzes"
component={PublicQuizzes}
options={{ title: 'Public Quizzes', headerShown: false }}
/>
</Drawer.Navigator>
);
}
export default function App() {
const initialLoginState = {
userToken: null,
};
const loginReducer = (prevState, action) => {
switch (action.type) {
case 'LOGIN':
return {
...prevState,
userToken: action.token,
};
case 'LOGOUT':
return {
...prevState,
userToken: null,
};
case 'REGISTER':
return {
...prevState,
userToken: action.token,
};
}
};
const [loginState, dispatch] = useReducer(loginReducer, initialLoginState);
const authContext = useMemo(() => ({
signIn: async (email, password) => {
try {
// Replace this with API-based authentication
let userToken = null;
if (email === "testuser@email.com" && password === "testpass") {
userToken = "testtoken";
await AsyncStorage.setItem("userToken", userToken);
}
dispatch({ type: "LOGIN", token: userToken });
} catch (error) {
console.error("Error during signIn:", error);
}
},
signOut: async () => {
try {
await AsyncStorage.removeItem("userToken");
dispatch({ type: "LOGOUT" });
} catch (error) {
console.error("Error during signOut:", error);
}
},
signUp: async (email, password) => {
try {
// Simulate user registration
const userToken = "newUserToken";
await AsyncStorage.setItem("userToken", userToken);
dispatch({ type: "REGISTER", token: userToken });
} catch (error) {
console.error("Error during signUp:", error);
}
},
}));
useEffect(() => {
const bootstrapAsync = async () => {
let userToken;
try {
userToken = await AsyncStorage.getItem("userToken");
} catch (error) {
console.error("Error fetching userToken:", error);
}
dispatch({ type: "LOGIN", token: userToken });
};
bootstrapAsync();
}, []);
return (
<AuthContext.Provider value={authContext}>
<NavigationContainer>
<Stack.Navigator initialRouteName="SplashScreen">
<Stack.Screen
name="SplashScreen"
component={SplashScreen}
options={{ headerShown: false }}
/>
<Stack.Screen
name="SignIn"
component={SignInScreen}
options={{ headerShown: false }}
/>
<Stack.Screen
name="SignUp"
component={SignUpScreen}
options={{ headerShown: false }}
/>
<Stack.Screen
name="MainDrawer"
component={DrawerNavigator}
options={{ headerShown: false }}
/>
<Stack.Screen
name="GameMode"
component={GameMode}
options={{
headerShown: false,
title: "Game Mode",
headerStyle: { backgroundColor: "#00C3FF" },
headerTintColor: "#fff",
headerTitleAlign: "center",
}}
/>
<Stack.Screen
name="ScrumModeScreen" // Ajout du mode Scrum
component={ScrumModeScreen}
options={{
headerShown: false,
title: 'Scrum Mode',
headerStyle: { backgroundColor: '#00C3FF' },
headerTintColor: '#fff',
headerTitleAlign: 'center',
}}
/>
<Stack.Screen
name="TeamScreen"
component={TeamScreen}
options={{
headerShown: true,
title: "Team Mode",
headerStyle: { backgroundColor: "#00C3FF" },
headerTintColor: "#fff",
headerTitleAlign: "center",
}}
/>
<Stack.Screen
name="QuizSettingsScreen"
component={QuizSettingsScreen}
options={{
headerShown: false,
title: "Quiz Settings",
headerStyle: { backgroundColor: "#00C3FF" },
headerTintColor: "#fff",
headerTitleAlign: "center",
}}
/>
<Stack.Screen
name="QuizScreen"
component={QuizScreen}
options={{
headerShown: false,
headerStyle: { backgroundColor: "#00C3FF" },
headerTintColor: "#fff",
title: "Quiz Details",
}}
/>
<Stack.Screen
name="QuizResultScreen"
component={QuizResultScreen}
options={{ headerShown: false }}
/>
</Stack.Navigator>
</NavigationContainer>
</AuthContext.Provider>
);
}
File moved
File moved
import AsyncStorage from "@react-native-async-storage/async-storage";
const API_BASE_URL = "http://localhost:2728";
let headers = {
"Content-Type": "application/json",
Authorization: "", // Dynamically updated with setAuthToken
......@@ -10,7 +12,6 @@ let headers = {
// Function to set the Authorization token dynamically
export const setAuthToken = async (token) => {
headers.Authorization = `Bearer ${token}`;
console.log("Setting auth token:", token); // Log the token being set
try {
await AsyncStorage.setItem("authToken", token); // Store token locally
} catch (error) {
......@@ -57,7 +58,12 @@ export const generateCreateQuizURL = async (amount, theme, difficulty) => {
throw new Error("No auth token found. Cannot create quiz.");
}
const url = `${API_BASE_URL}/createQuiz/${amount}/${theme}/${difficulty}`;
// Construire l'URL conditionnellement
let url = `${API_BASE_URL}/createQuiz?themeId=${theme}&nbQuestions=${amount}`;
if (difficulty) {
url += `&difficulty=${difficulty}`;
}
console.log("Generated Quiz URL:", url); // Debugging log
const response = await fetch(url, {
......@@ -72,7 +78,7 @@ export const generateCreateQuizURL = async (amount, theme, difficulty) => {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = (await response.json()).responseObject;
const data = await response.json();
console.log("Create Quiz Response:", data); // Debugging log
return data;
} catch (error) {
......@@ -80,71 +86,83 @@ export const generateCreateQuizURL = async (amount, theme, difficulty) => {
return { error: error.message };
}
};
export const generateGetQuizURL = async (quizId) => {
try {
const token = await getStoredAuthToken(); // Retrieve token from AsyncStorage
const token = await getStoredAuthToken(); // Récupérer le token
if (!token) {
throw new Error("No auth token found. Cannot fetch quiz.");
}
const url = `${API_BASE_URL}/play/${quizId}`;
// Construire l'URL avec le paramètre quizzId
const url = `${API_BASE_URL}/play?quizzId=${quizId}`;
console.log("Generated Get Quiz URL:", url);
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`, // Add token to Authorization header
Authorization: `Bearer ${token}`, // Ajouter le token
},
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
// Vérifier si le type de contenu est bien JSON
const contentType = response.headers.get("content-type");
if (!contentType || !contentType.includes("application/json")) {
const text = await response.text();
console.error("Unexpected response format:", text);
throw new Error(
"Unexpected response format. The server may have returned HTML instead of JSON."
);
}
const data = (await response.json()).responseObject;
console.log("Get Quiz Response:", data);
const data = await response.json();
return data;
} catch (error) {
console.error("Error fetching quiz:", error);
console.error("Error fetching quiz:", error.message);
return { error: error.message };
}
};
export const generateIsCorrectURL = async (quizId, questionId, answer) => {
export const generateIsCorrectURL = async (questionId, answerId) => {
try {
const token = await getStoredAuthToken();
const token = await getStoredAuthToken(); // Récupérer le token d'authentification
if (!token) {
throw new Error("No auth token found. Cannot verify answer.");
}
const url = `${API_BASE_URL}/isCorrect/${quizId}/${questionId}/${answer}`;
console.log("Generated Is Correct URL:", url);
// Construire correctement l'URL
const url = `${API_BASE_URL}/isCorrect?questionId=${questionId}&answerId=${answerId}`;
// Effectuer la requête
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
Authorization: `Bearer ${token}`, // Ajouter le token dans l'en-tête Authorization
},
});
console.log("Response status:", response.status);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = (await response.json()).responseObject;
console.log("Is Correct Response Data:", data);
const data = await response.json();
// Validate the structure of the data received
if (typeof data.isCorrect !== "boolean" || !("correctAnswer" in data)) {
// Valider la structure des données reçues
if (
typeof data.responseObject?.isCorrect !== "boolean" ||
!("correctAnswerId" in data.responseObject)
) {
throw new Error("Unexpected response format from the API.");
}
return data;
return data.responseObject;
} catch (error) {
console.error("Error verifying answer:", error.message);
return { error: error.message };
......@@ -158,8 +176,8 @@ export const generateRestartQuizURL = async (quizId) => {
throw new Error("No auth token found. Cannot restart quiz.");
}
const url = `${API_BASE_URL}/restartQuiz/${quizId}`;
console.log("Generated Restart Quiz URL:", url);
const url = `${API_BASE_URL}/restartQuiz?quizId=${quizId}`;
const response = await fetch(url, {
method: "PUT",
......@@ -170,15 +188,22 @@ export const generateRestartQuizURL = async (quizId) => {
});
if (!response.ok) {
const errorText = await response.text();
console.error("Error response body:", errorText);
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = (await response.json()).responseObject;
console.log("Restart Quiz Response:", data);
return data;
const data = await response.json();
if (!data?.responseObject?.questions || !data.responseObject.idCurrentQuestion) {
throw new Error("Invalid quiz data received.");
}
return data.responseObject;
} catch (error) {
console.error("Error restarting quiz:", error);
return { error: error.message };
console.error("Error restarting quiz:", error.message);
throw error; // Let the caller handle the error
}
};
......@@ -190,7 +215,6 @@ export const generateGetCategoriesURL = async () => {
}
const url = `${API_BASE_URL}/getCategories`;
console.log("Generated Get Categories URL:", url);
const response = await fetch(url, {
method: "POST",
......@@ -204,8 +228,8 @@ export const generateGetCategoriesURL = async () => {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = (await response.json()).responseObject;
console.log("Get Categories Response:", data);
const data = await response.json();
return data;
} catch (error) {
console.error("Error fetching categories:", error);
......@@ -222,7 +246,7 @@ export const getUserDetails = async () => {
}
const url = `${API_BASE_URL}/userDetails`;
console.log("Generated User Details URL:", url);
const response = await fetch(url, {
method: "POST", // Use POST method
......@@ -236,8 +260,7 @@ export const getUserDetails = async () => {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = (await response.json()).responseObject;
console.log("User Details Response:", data); // Debugging log
const data = await response.json();
return data;
} catch (error) {
console.error("Error fetching user details:", error);
......@@ -260,16 +283,14 @@ export const login = async (email, password) => {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const result = (await response.json()).responseObject;
console.log("Login response:", result); // Log the entire response
const result = await response.json();
// Correctly access the token
const token = result?.jwt;
const token = result?.result?.jwt;
if (token) {
console.log("Token received from login:", token); // Debugging log
await setAuthToken(token); // Automatically set token after login
console.log("Token successfully set after login:", token);
} else {
console.warn("Login did not return a token.");
}
......@@ -296,18 +317,14 @@ export const signup = async (nickname, email, password) => {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const result = (await response.json()).responseObject;
console.log("Signup response:", result); // Log the entire response
const result = await response.json();
// Correctly access the token
const token = result?.jwt;
const token = result?.result?.jwt;
if (token) {
console.log("Token received from signup:", token); // Debugging log
await setAuthToken(token); // Automatically set token after signup
console.log("Token successfully set after signup:", token);
} else {
console.warn("Signup did not return a token.");
}
return result;
......@@ -320,42 +337,38 @@ export const signup = async (nickname, email, password) => {
// Guest user creation function
export const createGuestUser = async () => {
try {
const response = await fetch(`${API_BASE_URL}/guest`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const result = (await response.json()).responseObject;
console.log("Guest user creation response:", result); // Log the entire response
// Access token correctly
const token = result?.jwt;
if (token) {
console.log("Token received from guest creation:", token); // Debugging log
await setAuthToken(token); // Automatically set token for guest users
console.log("Token successfully set after guest creation:", token);
} else {
console.warn("Guest user creation did not return a token.");
}
return result;
const response = await fetch(`${API_BASE_URL}/guest`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
// Retourne une erreur pour le composant appelant
return { error: `HTTP error! Status: ${response.status}` };
}
const result = await response.json();
const token = result?.responseObject?.jwt;
if (token) {
await setAuthToken(token); // Stocker le token localement
} else {
console.warn("Guest user creation did not return a token.");
}
return result; // Retourne le résultat complet au composant appelant
} catch (error) {
console.error("Error creating guest user:", error);
return { error: error.message };
console.error("Error creating guest user:", error);
return { error: error.message }; // Retourne l'erreur pour le composant appelant
}
};
export const disconnect = async (navigation) => {
try {
await AsyncStorage.removeItem("authToken"); // Supprimer le token stocké
console.log("Token successfully removed.");
navigation.navigate("SplashScreen"); // Rediriger vers l'écran de démarrage
} catch (error) {
console.error("Error during disconnect:", error);
......@@ -368,14 +381,19 @@ export const disconnect = async (navigation) => {
export const fetchPublicQuizzes = async (page = 1, pageSize = 10) => {
const url = `${API_BASE_URL}/publicQuizzes?page=${page}&pageSize=${pageSize}`;
const token = await AsyncStorage.getItem("authToken");
try {
const token = await AsyncStorage.getItem("authToken");
if (!token) {
throw new Error("No auth token found. Please log in again.");
}
const response = await fetch(url, {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`,
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
});
......@@ -385,10 +403,8 @@ export const fetchPublicQuizzes = async (page = 1, pageSize = 10) => {
throw new Error(`API Error: ${errorText}`);
}
const data = (await response.json()).responseObject;
console.log("API Response:", data);
const data = await response.json();
return data || [];
// Renvoie les quizzes ou un tableau vide
} catch (error) {
console.error("Error fetching public quizzes:", error.message);
throw error;
......@@ -397,10 +413,13 @@ export const fetchPublicQuizzes = async (page = 1, pageSize = 10) => {
//played quizzes
export const fetchUserDetails = async () => {
export const fetchUserDetails = async (page = 1, pageSize = 10) => {
const token = await AsyncStorage.getItem("authToken");
try {
const response = await fetch(`${API_BASE_URL}/userDetails`, {
const url = `${API_BASE_URL}/userDetails?page=${page}&pageSize=${pageSize}`;
const response = await fetch(url, {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`,
......@@ -412,8 +431,14 @@ export const fetchUserDetails = async () => {
throw new Error("Failed to fetch user details");
}
const data = (await response.json()).responseObject;
return data.history || []; // Retourne uniquement l'historique des quizzes joués
const data = await response.json();
if (data && data.responseObject && data.responseObject.history) {
return data.responseObject.history; // Retourne uniquement l'historique des quizzes joués
} else {
console.warn("No history found in response.");
return []; // Retourne un tableau vide si aucun historique n'est trouvé
}
} catch (error) {
console.error("Error fetching user details:", error);
throw error;
......
// server.js
const express = require("express");
const http = require("http");
const { Server } = require("socket.io");
const app = express();
const server = http.createServer(app);
const io = new Server(server);
const PORT = 3000;
/**
* Structure d’un lobby en mode team :
* lobbies[lobbyId] = {
* quizId, // identifiant du quiz
* quizData, // le quiz complet (questions, réponses, correctAnswerId, etc.)
* teams: [ // liste des équipes
* {
* name,
* players: [
* { id, score, hasAnswered, lastActivity }
* ]
* },
* ...
* ],
* host: socket.id, // identifiant du créateur
* hasStarted, // true si la partie a démarré
* currentQuestionIndex,
* difficulty, // niveau de difficulté pour le temps de réponse (ex: "facile", "moyen", "difficile")
* isTimerMode, // true si le mode timer est activé
* };
*/
let lobbies = {};
// Vérification de l'inactivité toutes les 10 secondes
const INACTIVITY_LIMIT = 30 * 1000; // 30 secondes
setInterval(() => {
const now = Date.now();
for (const lobbyId in lobbies) {
const lobby = lobbies[lobbyId];
if (!lobby) continue;
if (!lobby.hasStarted) continue;
// Vérifier l'inactivité pour chaque joueur dans chaque équipe
lobby.teams.forEach(team => {
team.players = team.players.filter(player => {
const inactiveTime = now - (player.lastActivity || 0);
if (inactiveTime > INACTIVITY_LIMIT) {
console.log(`Player ${player.id} removed for inactivity in lobby ${lobbyId} team ${team.name}`);
return false;
}
return true;
});
});
// Si toutes les équipes sont vides, supprimer le lobby
if (lobby.teams.every(team => team.players.length === 0)) {
console.log(`Lobby ${lobbyId} is empty and will be deleted (inactivity).`);
delete lobbies[lobbyId];
} else {
io.in(lobbyId).emit("lobbyUpdate", lobby);
}
}
}, 10 * 1000);
io.on("connection", (socket) => {
console.log("A user connected:", socket.id);
socket.lastActivity = Date.now();
// Mettre à jour l'activité d'un joueur dans une équipe d'un lobby
const updatePlayerActivity = (lobbyId, playerId) => {
const lobby = lobbies[lobbyId];
if (!lobby) return;
lobby.teams.forEach(team => {
team.players.forEach(player => {
if (player.id === playerId) {
player.lastActivity = Date.now();
}
});
});
};
// 1) Création d'un lobby team
// Données attendues : { lobbyId, quizId, quizData, teams } => teams est un tableau d'objets { name }
socket.on("createTeamLobby", ({ lobbyId, quizId, quizData, teams }, callback) => {
if (lobbies[lobbyId]) {
return callback({ error: "Lobby ID already exists." });
}
// Création de la structure du lobby en mode team
lobbies[lobbyId] = {
quizId,
quizData: quizData || null,
teams: teams.map(team => ({
name: team.name,
players: []
})),
host: socket.id,
hasStarted: false,
currentQuestionIndex: 0,
difficulty: null,
isTimerMode: false,
};
// Ajout du créateur dans la première équipe par défaut (le choix peut être modifié)
lobbies[lobbyId].teams[0].players.push({
id: socket.id,
score: 0,
hasAnswered: false,
lastActivity: Date.now(),
});
socket.join(lobbyId);
console.log(`Team lobby ${lobbyId} created by player ${socket.id}`);
callback({ lobbyId, teams: lobbies[lobbyId].teams, host: socket.id });
io.in(lobbyId).emit("lobbyUpdate", lobbies[lobbyId]);
});
// 2) Rejoindre une équipe dans un lobby existant
// Données attendues : { lobbyId, playerId, teamName }
socket.on("joinTeam", ({ lobbyId, playerId, teamName }, callback) => {
const lobby = lobbies[lobbyId];
if (!lobby) {
return callback({ error: "Lobby does not exist." });
}
if (lobby.hasStarted) {
return callback({ error: "Game already started. You cannot join now." });
}
// Vérifier que le joueur n'est pas déjà inscrit dans une équipe
for (const team of lobby.teams) {
if (team.players.some(p => p.id === playerId)) {
return callback({ error: "You are already in a team in this lobby." });
}
}
const team = lobby.teams.find(t => t.name === teamName);
if (!team) {
return callback({ error: "Team not found." });
}
team.players.push({
id: playerId,
score: 0,
hasAnswered: false,
lastActivity: Date.now(),
});
socket.join(lobbyId);
console.log(`Player ${playerId} joined team ${teamName} in lobby ${lobbyId}`);
io.in(lobbyId).emit("lobbyUpdate", lobby);
callback({ success: true });
});
// 3) Démarrer la partie en mode team
// Données attendues : { lobbyId, quizData, difficulty, isTimerMode }
socket.on("startTeamGame", (data, callback) => {
const lobbyId = data.lobbyId;
const lobby = lobbies[lobbyId];
if (!lobby) {
return callback({ error: "Lobby does not exist." });
}
if (socket.id !== lobby.host) {
return callback({ error: "Only the host can start the game." });
}
lobby.hasStarted = true;
lobby.currentQuestionIndex = 0;
lobby.difficulty = data.difficulty;
lobby.isTimerMode = data.isTimerMode;
if (data.quizData) {
lobby.quizData = data.quizData;
}
console.log(`Team game started in lobby ${lobbyId}`);
io.in(lobbyId).emit("gameStarted", {
quizData: lobby.quizData,
difficulty: lobby.difficulty,
isTimerMode: lobby.isTimerMode,
questionIndex: lobby.currentQuestionIndex,
});
callback({ success: true });
});
// 4) Réception d'une réponse d'un joueur en mode team
// Données attendues : { lobbyId, playerId, teamName, questionId, answerId }
socket.on("submitTeamAnswer", ({ lobbyId, playerId, teamName, questionId, answerId }, callback) => {
const lobby = lobbies[lobbyId];
if (!lobby || !lobby.hasStarted) {
return callback({ error: "Game not started or lobby does not exist." });
}
const team = lobby.teams.find(t => t.name === teamName);
if (!team) {
return callback({ error: "Team not found." });
}
const player = team.players.find(p => p.id === playerId);
if (!player) {
return callback({ error: "Player not found in team." });
}
if (player.hasAnswered) {
return callback({ error: "You have already answered this question." });
}
player.hasAnswered = true;
updatePlayerActivity(lobbyId, playerId);
const currentQuestion = lobby.quizData.questions[lobby.currentQuestionIndex];
if (!currentQuestion) {
return callback({ error: "Question not found." });
}
const isCorrect = answerId === currentQuestion.correctAnswerId;
if (isCorrect) {
player.score += 10;
}
// Vérifier si tous les joueurs de toutes les équipes ont répondu
let allAnswered = true;
lobby.teams.forEach(team => {
team.players.forEach(p => {
if (!p.hasAnswered) {
allAnswered = false;
}
});
});
if (allAnswered) {
// Calculer la moyenne de score de chaque équipe
const teamsScores = lobby.teams.map(team => {
const totalScore = team.players.reduce((acc, p) => acc + p.score, 0);
const averageScore = team.players.length ? totalScore / team.players.length : 0;
return { teamName: team.name, averageScore, players: team.players };
});
io.in(lobbyId).emit("questionResults", {
teamsScores,
questionIndex: lobby.currentQuestionIndex,
});
// Passage à la question suivante
lobby.currentQuestionIndex++;
if (lobby.currentQuestionIndex < lobby.quizData.questions.length) {
// Réinitialiser le statut "hasAnswered" pour tous les joueurs
lobby.teams.forEach(team => {
team.players.forEach(p => {
p.hasAnswered = false;
});
});
io.in(lobbyId).emit("newQuestion", {
questionIndex: lobby.currentQuestionIndex,
});
} else {
io.in(lobbyId).emit("gameEnded", { teamsScores });
}
}
callback({ success: true });
});
// 5) Quitter un lobby team
// Données attendues : { lobbyId, playerId }
socket.on("leaveTeam", ({ lobbyId, playerId }, callback) => {
const lobby = lobbies[lobbyId];
if (!lobby) return callback({ error: "Lobby not found." });
lobby.teams.forEach(team => {
team.players = team.players.filter(p => p.id !== playerId);
});
socket.leave(lobbyId);
if (lobby.teams.every(team => team.players.length === 0)) {
console.log(`Lobby ${lobbyId} is empty and will be deleted.`);
delete lobbies[lobbyId];
} else {
io.in(lobbyId).emit("lobbyUpdate", lobby);
}
callback({ success: true });
});
// 6) Mettre à jour lastActivity sur tout événement (sauf disconnect)
socket.onAny((eventName, ...args) => {
if (eventName !== "disconnect") {
socket.lastActivity = Date.now();
}
});
// 7) Déconnexion
socket.on("disconnect", () => {
console.log("User disconnected:", socket.id);
for (const lobbyId in lobbies) {
const lobby = lobbies[lobbyId];
let removed = false;
lobby.teams.forEach(team => {
const index = team.players.findIndex(p => p.id === socket.id);
if (index !== -1) {
team.players.splice(index, 1);
removed = true;
}
});
if (removed) {
if (lobby.teams.every(team => team.players.length === 0)) {
console.log(`Lobby ${lobbyId} is empty and will be deleted.`);
delete lobbies[lobbyId];
} else {
io.in(lobbyId).emit("lobbyUpdate", lobby);
}
}
}
});
});
server.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
\ No newline at end of file
......@@ -61,7 +61,7 @@ const CategorySelector = () => {
const handleStartQuiz = () => {
if (selectedCategory) {
navigation.navigate('QuizSettingsScreen', { category: selectedCategory });
navigation.navigate('GameMode', { category: selectedCategory });
} else {
Alert.alert('Please select a category before starting the quiz.');
}
......@@ -75,22 +75,26 @@ const CategorySelector = () => {
try {
const url = await generateGetQuizURL(quizCode);
console.log('Generated Get Quiz URL:', url);
const quizData = url;
console.log('Generated Get Quiz URL:', url);
if (quizData && quizData.questions && quizData.questions.length > 0) {
// Navigate to QuizScreen if quiz data is valid
navigation.navigate('QuizScreen', { quizData });
} else {
const quizData = url.responseObject; // Extraction correcte des données
if (!quizData || !quizData.questions || !Array.isArray(quizData.questions)) {
console.error('Invalid quiz data structure:', quizData);
Alert.alert('Error', 'Quiz data is invalid or does not exist.');
return;
}
if (quizData.questions.length > 0) {
// Navigate to QuizScreen si les données sont valides
navigation.navigate('QuizScreen', { quizData });
} else {
Alert.alert('Error', 'The quiz has no questions.');
}
} catch (error) {
console.error('Error fetching quiz:', error);
Alert.alert('Error', 'Failed to verify the quiz code.');
}
};
return (
<View style={styles.container}>
<View style={styles.header}>
......
import React, { useState } from "react";
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
} from "react-native";
const ForgotPasswordScreen = ({ navigation }) => {
const [email, setEmail] = useState("");
const handleSendCode = () => {
// Logique d'envoi de code
};
return (
<View style={styles.container}>
<View style={styles.card}>
<View style={styles.topSection}>
<Text style={styles.headerText}>Forgot Password?</Text>
<Text style={styles.subText}>
Dont worry! It occurs. Please enter the email address linked with
your account.
</Text>
</View>
<View style={styles.middleSection}>
<TextInput
style={styles.input}
placeholder="Enter your email"
keyboardType="email-address"
autoCapitalize="none"
onChangeText={(text) => setEmail(text)}
value={email}
/>
<TouchableOpacity style={styles.button} onPress={handleSendCode}>
<Text style={styles.buttonText}>Send Code</Text>
</TouchableOpacity>
</View>
<TouchableOpacity
style={styles.footer}
onPress={() => navigation.navigate("SignIn")}
>
<Text style={styles.footerText}>
Remember Password?{" "}
<Text style={styles.linkText}>Login Now</Text>
</Text>
</TouchableOpacity>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#00C3FF",
justifyContent: "center",
alignItems: "center",
paddingVertical: 40,
},
card: {
backgroundColor: "#fff",
borderRadius: 40,
padding: 20,
width: "90%",
height: "90%", // Fond blanc agrandi
elevation: 10,
shadowColor: "#000",
shadowOpacity: 0.2,
shadowRadius: 10,
shadowOffset: { width: 0, height: 5 },
justifyContent: "space-between", // Sections haut, milieu, bas
},
topSection: {
marginBottom: 30, // Plus d'espace entre "Forgot Password?" et les champs
},
headerText: {
fontSize: 33,
fontWeight: "bold",
color: "#000",
textAlign: "center",
marginBottom: 20, // Ajout d'un espace entre le titre et la description
},
subText: {
fontSize: 14,
color: "#000",
textAlign: "center",
lineHeight: 20,
},
middleSection: {
flexGrow: 1,
justifyContent: "flex-start",
},
input: {
width: "100%",
backgroundColor: "#F4F4F4",
borderRadius: 15,
padding: 15,
borderWidth: 1,
borderColor: "#DDD",
marginBottom: 40,
},
button: {
backgroundColor: "#00C3FF",
paddingVertical: 15,
borderRadius: 10,
alignItems: "center",
width: "100%",
},
buttonText: {
color: "#fff",
fontWeight: "bold",
fontSize: 16,
},
footer: {
alignSelf: "center", // Centrer en bas
},
footerText: {
color: "#000",
fontSize: 14,
textAlign: "center",
},
linkText: {
color: "#00C3FF",
fontWeight: "bold",
},
});
export default ForgotPasswordScreen;
import React, { useState } from "react";
import {
View,
Text,
TouchableOpacity,
StyleSheet,
Alert,
} from "react-native";
import { Ionicons, FontAwesome5, MaterialCommunityIcons } from "@expo/vector-icons";
import { useNavigation } from "@react-navigation/native";
const GameMode = ({ route }) => {
const navigation = useNavigation();
const { category } = route.params;
const [selectedMode, setSelectedMode] = useState(null);
const handleNext = () => {
if (!selectedMode) {
Alert.alert("Error", "Please select a game mode.");
return;
}
if (selectedMode === "Solo standard") {
navigation.navigate("QuizSettingsScreen", { category, isTimerMode: false });
} else if (selectedMode === "Solo with timer") {
navigation.navigate("QuizSettingsScreen", { category, isTimerMode: true });
// } else if (selectedMode === "Scrum mode") {
// // Au lieu de passer seulement "category", on ajoute "isScrumMode: true"
// navigation.navigate("QuizSettingsScreen", {
// category,
// isTimerMode: false, // le timer n'est pas actif en Scrum
// isScrumMode: true, // <-- important
// });
// } else if (selectedMode === "Team mode") {
// navigation.navigate("TeamScreen", { category });
} else {
Alert.alert(
"Coming Soon",
"This game mode is not yet implemented. Stay tuned!"
);
}
};
const renderCategoryIcon = () => {
const IconComponent =
category.library === "FontAwesome5"
? FontAwesome5
: category.library === "MaterialCommunityIcons"
? MaterialCommunityIcons
: Ionicons;
return (
<IconComponent
name={category.icon}
size={60}
color="white"
style={styles.categoryIcon}
/>
);
};
// Les options affichées contiennent uniquement les modes Solo.
// Les modes "Scrum mode" et "Team mode" sont conservés en commentaire.
const gameModes = [
"Solo standard",
"Solo with timer",
// "Scrum mode",
// "Team mode"
];
return (
<View style={styles.container}>
<TouchableOpacity style={styles.backButton} onPress={() => navigation.goBack()}>
<Ionicons name="arrow-back" size={24} color="white" />
</TouchableOpacity>
<View style={styles.header}>
{renderCategoryIcon()}
<Text style={styles.headerText}>{category ? category.name : "Category"}</Text>
</View>
<View style={styles.content}>
<Text style={styles.label}>Select Game Mode</Text>
{/* Options des modes */}
<View style={styles.optionsContainer}>
{gameModes.map((mode) => (
<TouchableOpacity
key={mode}
style={[
styles.option,
selectedMode === mode && styles.selectedOption,
]}
onPress={() => setSelectedMode(mode)}
>
<Text
style={[
styles.optionText,
selectedMode === mode && styles.selectedOptionText,
]}
>
{mode}
</Text>
</TouchableOpacity>
))}
</View>
</View>
<TouchableOpacity style={styles.nextButton} onPress={handleNext}>
<Text style={styles.nextButtonText}>Next</Text>
</TouchableOpacity>
</View>
);
};
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: "#00C3FF" },
backButton: { position: "absolute", top: 60, left: 20, zIndex: 1 },
header: { alignItems: "center", paddingVertical: 20, marginTop: 50 },
categoryIcon: { marginTop: 10 },
headerText: { color: "white", fontSize: 24, fontWeight: "bold", marginTop: 10 },
content: {
backgroundColor: "#fff",
borderTopLeftRadius: 30,
borderTopRightRadius: 30,
padding: 20,
flex: 1,
marginTop: 50,
},
label: {
fontSize: 18,
fontWeight: "bold",
color: "black",
marginBottom: -50,
textAlign: "left",
},
optionsContainer: {
justifyContent: "center",
alignItems: "center",
flexGrow: 1,
},
option: {
paddingVertical: 25,
paddingHorizontal: 60,
borderRadius: 10,
borderWidth: 1,
borderColor: "gray",
backgroundColor: "transparent",
marginBottom: 25,
alignItems: "center",
width: "80%",
},
selectedOption: {
backgroundColor: "#00C3FF",
borderColor: "#00C3FF",
},
optionText: {
fontSize: 18,
color: "gray",
fontWeight: "bold",
},
selectedOptionText: {
color: "white",
},
nextButton: {
backgroundColor: "#00C3FF",
paddingVertical: 15,
borderRadius: 10,
alignItems: "center",
position: "absolute",
bottom: 40,
width: "70%",
alignSelf: "center",
},
nextButtonText: { color: "white", fontSize: 18, fontWeight: "bold" },
});
export default GameMode;
\ No newline at end of file
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment