pull/84/head
Pierre Ferreira 1 year ago
commit 889367c88a

3
.gitignore vendored

@ -43,6 +43,9 @@ psd
thumb thumb
sketch sketch
### modules ###
yarn.lock
package-lock.json
# db # db

@ -28,37 +28,50 @@ Sur **Windows**
Sur **MacOS** / **Linux** Sur **MacOS** / **Linux**
> - Ouvrez le terminal et tapez la commande `ifconfig` ou `ip addr`. Recherchez la section de votre connexion sans fil et notez l'adresse IP. > - Ouvrez le terminal et tapez la commande `ifconfig` ou `ip addr`. Recherchez la section de votre connexion sans fil et notez l'adresse IP.
### Etape 3 : Configurer Socket.IO ### Etape 3 : Configurer les serveurs
1. **Ouvrez le fichier `./src/SocketConfig.ts` :** 1. **Ouvrez le fichier `./src/AdressSetup.ts` :**
- Localisez le fichier dans le répertoire de votre application. - Localisez le fichier dans le répertoire de votre application.
2. **Modifiez l'adresse IP dans la ligne `const socket = io(...)` :** 2. **Modifiez l'adresse des serveurs :**
- Remplacez l'adresse IP existante par celle que vous avez notée à l'étape 2. - Remplacez les adresses IP existantes par celle que vous avez notée à l'étape 2.
Exemple : Exemple :
```typescript ```typescript
//SocketConfig.ts // ./AdressSetup.ts
import { io } from "socket.io-client"; const ADRESSE_WEBSERVER = "http://{VOTRE_IP}:3002"
const ADRESSE_DBSERVER = "http://{VOTRE_IP}:3003"
const ADRESSE_WEBSITE = ""
// Remplacez "http://172.20.10.4:3002" par votre propre adresse IP export {ADRESSE_DBSERVER, ADRESSE_WEBSERVER, ADRESSE_WEBSITE}
const socket = io("http://VOTRE_ADRESSE_IP:3002"); ```
3. **Ouvrez le fichier `./server/server.js` :**
- Localisez le fichier dans le répertoire de votre application.
export { socket }; 4. **Modifiez les adresses qui peuvent accèder aux serveurs :**
- Ajoutez votre adresse notée à l'étape 2 dans le cors.
Exemple :
```typescript
const app = express();
const server = http.createServer(app);
const io = socketIO(server, {
cors: {
origin: ["http://{VOTRE_IP}:3000", "http://localhost:3000"], // Remplacez par l'URL de votre application React
methods: ["GET", "POST"],
credentials: true
}
});
``` ```
5. **Apportez la même modification au fichier `./src/server/server.js` :**
- Une fois le fichier ouvert, appuyez-vous sur l'exemple précédent pour apporter les modifications necéssaire.
### Etape 4 : Démarrer les serveurs ### Etape 4 : Démarrer les serveurs
Dans un second terminal, ouvrez le serveur Socket.IO :
```bash
cd ./server
node server.js
```
Dans un troisième ouvrez le serveur gérant l'API 1. **Ouvrez un second terminal :**
```bash - Exécutez le script `./startServer.sh`.
cd ./src/server
node server.js
```
### Etape 5 : Démarrer l'application ### Etape 5 : Démarrer l'application

@ -0,0 +1,7 @@
const ADRESSE_WEBSERVER = "http://localhost:3002"
const ADRESSE_DBSERVER = "http://localhost:3003"
const ADRESSE_WEBSITE = ""
export {ADRESSE_DBSERVER, ADRESSE_WEBSERVER, ADRESSE_WEBSITE}

@ -10,6 +10,7 @@ import Home from './Pages/Home';
import Login from './Pages/LoginForm'; import Login from './Pages/LoginForm';
import SignUp from './Pages/SignUpForm'; import SignUp from './Pages/SignUpForm';
import Play from './Pages/Play'; import Play from './Pages/Play';
import Profile from './Pages/Profile';
import Lobby from './Pages/Lobby'; import Lobby from './Pages/Lobby';
import InGame from './Pages/InGame'; import InGame from './Pages/InGame';
@ -84,6 +85,7 @@ function App() {
<Route path="/endgame" element={<EndGame/>} /> <Route path="/endgame" element={<EndGame/>} />
<Route path="/game" element={<InGame locale={locale} changeLocale={changeLocale}/>}/> <Route path="/game" element={<InGame locale={locale} changeLocale={changeLocale}/>}/>
<Route path="/info" element={<InfoPage locale={locale} changeLocale={changeLocale}/>} /> <Route path="/info" element={<InfoPage locale={locale} changeLocale={changeLocale}/>} />
<Route path="/profile" element={<Profile/>} />
{/* <Route path="/solo" element={<SoloGame locale={locale} changeLocale={changeLocale} />}/> */} {/* <Route path="/solo" element={<SoloGame locale={locale} changeLocale={changeLocale} />}/> */}
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>

@ -13,7 +13,6 @@ import Bot from "../model/Bot";
import NodePerson from "../model/Graph/NodePerson"; import NodePerson from "../model/Graph/NodePerson";
import { useAuth } from "../Contexts/AuthContext"; import { useAuth } from "../Contexts/AuthContext";
interface MyGraphComponentProps { interface MyGraphComponentProps {
onNodeClick: (shouldShowChoiceBar: boolean) => void; onNodeClick: (shouldShowChoiceBar: boolean) => void;
handleShowTurnBar: (shouldShowTurnBar: boolean) => void handleShowTurnBar: (shouldShowTurnBar: boolean) => void
@ -40,22 +39,42 @@ let lastSocketId= ""
let firstLap = true let firstLap = true
let cptHistory = 0 let cptHistory = 0
let lastNodes: NodePerson[] = [] let lastNodes: NodePerson[] = []
let cptEndgame = 0
let firstEnigme = true let firstEnigme = true
let endgame= false
const MyGraphComponent: React.FC<MyGraphComponentProps> = ({onNodeClick, handleShowTurnBar, handleTurnBarTextChange, playerTouched, setPlayerTouched, changecptTour, solo, isDaily, addToHistory, showLast, setNetwork}) => { const MyGraphComponent: React.FC<MyGraphComponentProps> = ({onNodeClick, handleShowTurnBar, handleTurnBarTextChange, playerTouched, setPlayerTouched, changecptTour, solo, isDaily, addToHistory, showLast, setNetwork}) => {
let cptTour: number = 0 let cptTour: number = 0
//* Gestion du temps : //* Gestion du temps :
const initMtn = new Date().getSeconds() let initMtn = 0
const {user} = useAuth() const {isLoggedIn, user, manager} = useAuth();
const { indices, indice, person, personNetwork, setNodeIdData, players, askedPersons, setActualPlayerIndexData, room, actualPlayerIndex, turnPlayerIndex, setTurnPlayerIndexData, setWinnerData, dailyEnigme, setNbCoupData, settempsData} = useGame(); const { indices, indice, person, personNetwork, setNodeIdData, players, askedPersons, setActualPlayerIndexData, room, actualPlayerIndex, turnPlayerIndex, setTurnPlayerIndexData, setWinnerData, dailyEnigme, setNbCoupData, settempsData} = useGame();
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const navigate = useNavigate(); const navigate = useNavigate();
const [lastIndex, setLastIndex] = useState(-1) const [lastIndex, setLastIndex] = useState(-1)
const [elapsedTime, setElapsedTime] = useState(0);
useEffect(() => {
// Démarrez le timer au montage du composant
const intervalId = setInterval(() => {
setElapsedTime((prevElapsedTime) => prevElapsedTime + 0.5);
settempsData(elapsedTime)
// Vérifiez si la durée est écoulée, puis arrêtez le timer
if (endgame) {
clearInterval(intervalId);
}
}, 500);
// Nettoyez l'intervalle lorsque le composant est démonté
return () => clearInterval(intervalId);
}, [elapsedTime, endgame]);
useEffect(() =>{ useEffect(() =>{
touchedPlayer=playerTouched touchedPlayer=playerTouched
@ -201,7 +220,7 @@ const MyGraphComponent: React.FC<MyGraphComponentProps> = ({onNodeClick, handleS
if (first){ if (first){
first = false first = false
endgame= false
if (!solo){ if (!solo){
for(let i = 0; i<indices.length; i++){ for(let i = 0; i<indices.length; i++){
mapIndexPersons.set(i, []) mapIndexPersons.set(i, [])
@ -267,24 +286,21 @@ const MyGraphComponent: React.FC<MyGraphComponentProps> = ({onNodeClick, handleS
dailyEnigme.forEach((pairs, index) => { dailyEnigme.forEach((pairs, index) => {
pairs.forEach((pair) => { pairs.forEach((pair) => {
const i = indices.findIndex((indice) => pair.first.getId() === indice.getId()) const i = indices.findIndex((indice) => pair.first.getId() === indice.getId())
console.log(index)
const node = networkData.nodes.get().find((n) => index == n.id) const node = networkData.nodes.get().find((n) => index == n.id)
if (node != undefined){ if (node != undefined){
networkData.nodes.update({id: node.id, label: node.label + positionToEmoji(i, pair.second)}) networkData.nodes.update({id: node.id, label: node.label + positionToEmoji(i, pair.second)})
const test = networkData.nodes.get().find((n) => index == n.id) const test = networkData.nodes.get().find((n) => index == n.id)
if (test!=undefined){
console.log(test.label)
}
} }
}) })
}); });
} }
indices.forEach((i, index) => { socket.on("reset graph", () => {
console.log(i.ToString("fr") + " => " + positionToEmoji(index, true)) console.log("reset graph")
initialOptions.physics.enabled = true
network.setOptions(initialOptions)
}) })
if (!solo){ if (!solo){
socket.on("asked all", (id) =>{ socket.on("asked all", (id) =>{
const pers = personNetwork.getPersons().find((p) => p.getId() == id) const pers = personNetwork.getPersons().find((p) => p.getId() == id)
@ -307,13 +323,7 @@ const MyGraphComponent: React.FC<MyGraphComponentProps> = ({onNodeClick, handleS
}); });
}) })
socket.on("reset graph", () => {
initialOptions.physics.enabled = true
network.setOptions(initialOptions)
})
socket.on("node checked",(id, works, askedIndex, newPlayerIndex, socketId) => { socket.on("node checked",(id, works, askedIndex, newPlayerIndex, socketId) => {
console.log(newPlayerIndex)
const node = nodes.get().find((n) => id == n.id) const node = nodes.get().find((n) => id == n.id)
if (node!=undefined){ if (node!=undefined){
onNodeClick(false) onNodeClick(false)
@ -408,7 +418,6 @@ const MyGraphComponent: React.FC<MyGraphComponentProps> = ({onNodeClick, handleS
socket.emit("node checked", nodeId, false, actualPlayerIndex, room, index) socket.emit("node checked", nodeId, false, actualPlayerIndex, room, index)
socket.emit("asked wrong", askingPlayer, room) socket.emit("asked wrong", askingPlayer, room)
} }
} }
} }
} }
@ -475,15 +484,49 @@ const MyGraphComponent: React.FC<MyGraphComponentProps> = ({onNodeClick, handleS
}) })
socket.on("end game", (winnerIndex) =>{ socket.on("end game", (winnerIndex) =>{
if (cptEndgame % 2 == 0){
cptEndgame++;
const currentPlayer = players[actualPlayerIndex];
const winner = players[winnerIndex];
setNodeIdData(-1) setNodeIdData(-1)
setActualPlayerIndexData(-1) setActualPlayerIndexData(-1)
setLastIndex(-1) setLastIndex(-1)
setPlayerTouched(-1) setPlayerTouched(-1)
setWinnerData(players[winnerIndex]) setWinnerData(winner)
setElapsedTime(0)
first = true first = true
cptHistory = 0 cptHistory = 0
askedWrong=false askedWrong=false
askedWrongBot=false askedWrongBot=false
endgame = true
try{
if(isLoggedIn){
if(!solo){
if(user && user.onlineStats){
// console.log("nbGames: " + user.onlineStats.nbGames + " nbWins: " + user.onlineStats.nbWins);
if(winner.id === currentPlayer.id){
// Ajouter une victoire
user.onlineStats.nbWins = null ? user.onlineStats.nbWins = 1 : user.onlineStats.nbWins += 1;
}
// Update les stats
user.onlineStats.nbGames = null ? user.onlineStats.nbGames = 1 : user.onlineStats.nbGames += 1;
user.onlineStats.ratio = user.onlineStats.nbWins / user.onlineStats.nbGames;
manager.userService.updateOnlineStats(user.pseudo, user.onlineStats.nbGames, user.onlineStats.nbWins, user.onlineStats.ratio);
}
else{
console.error("User not found");
}
}
}
}
catch(e){
console.log(e);
}
finally{
socket.off("end game") socket.off("end game")
socket.off("asked all") socket.off("asked all")
socket.off("opacity activated") socket.off("opacity activated")
@ -498,6 +541,8 @@ const MyGraphComponent: React.FC<MyGraphComponentProps> = ({onNodeClick, handleS
socket.off("put imossible grey") socket.off("put imossible grey")
navigate("/endgame") navigate("/endgame")
}
}
}) })
@ -512,7 +557,7 @@ const MyGraphComponent: React.FC<MyGraphComponentProps> = ({onNodeClick, handleS
} }
if (a==indices.length){ if (a==indices.length){
//networkData.nodes.update({id: p.getId(), label: p.getName() + "\n🔵"}) //networkData.nodes.update({id: p.getId(), label: p.getName() + "\n🔵"})
console.log(p) //console.log(p)
} }
}); });
@ -651,12 +696,30 @@ const MyGraphComponent: React.FC<MyGraphComponentProps> = ({onNodeClick, handleS
works = false works = false
} }
if (index == indices.length - 1 && works){ if (index == indices.length - 1 && works){
const Mtn = new Date().getSeconds()
settempsData(Mtn - initMtn)
if (user!=null){
setWinnerData(user)
}
cptTour ++; cptTour ++;
setNbCoupData(cptTour) setNbCoupData(cptTour)
setElapsedTime(0)
endgame = true
try{
if(user && user.soloStats){
user.soloStats.nbGames = null ? user.soloStats.nbGames = 1 : user.soloStats.nbGames += 1;
if(cptTour < user.soloStats.bestScore || user.soloStats.bestScore == 0 || user.soloStats.bestScore == null){
user.soloStats.bestScore = cptTour;
}
user.soloStats.avgNbTry = (user.soloStats.avgNbTry * (user.soloStats.nbGames - 1) + cptTour) / user.soloStats.nbGames;
manager.userService.updateSoloStats(user.pseudo, user.soloStats.nbGames, user.soloStats.bestScore, user.soloStats.avgNbTry);
}
}
catch(error){
console.log(error);
}
navigate("/endgame?solo=true&daily=" + isDaily) navigate("/endgame?solo=true&daily=" + isDaily)
} }

@ -23,16 +23,31 @@ import './NavBar.css';
/* Style */ /* Style */
import { useTheme } from '../Style/ThemeContext'; import { useTheme } from '../Style/ThemeContext';
import { useAuth } from '../Contexts/AuthContext'; import { useAuth } from '../Contexts/AuthContext';
import { useNavigate } from 'react-router-dom';
// @ts-ignore // @ts-ignore
function AppNavbar({changeLocale}) { function AppNavbar({changeLocale}) {
const theme = useTheme(); const theme = useTheme();
const {isLoggedIn, logout} = useAuth(); const {user, isLoggedIn, logout} = useAuth();
const navigate = useNavigate();
useEffect(() => {
console.log(user)
}, [user])
function navigateToProfile(){
navigate("/profile")
}
function navigateToHome(){
navigate("/")
}
return ( return (
<Navbar expand="lg" className="custom-navbar" style={{ backgroundColor: theme.colors.primary }}> <Navbar expand="lg" className="custom-navbar" style={{ backgroundColor: theme.colors.primary }}>
<Container> <Container>
<Navbar.Brand href="/"> <Navbar.Brand onClick={navigateToHome}>
<img src={logo} alt="logo" className="logo" /> <img src={logo} alt="logo" className="logo" />
</Navbar.Brand> </Navbar.Brand>
<Navbar.Toggle aria-controls="basic-navbar-nav" /> <Navbar.Toggle aria-controls="basic-navbar-nav" />
@ -54,7 +69,7 @@ function AppNavbar({changeLocale}) {
align="end" align="end"
drop='down-centered' drop='down-centered'
> >
<NavDropdown.Item href="/profile">Profil</NavDropdown.Item> <NavDropdown.Item onClick={navigateToProfile}>Profil</NavDropdown.Item>
<LanguageNavItem <LanguageNavItem
countryCode="FR" countryCode="FR"
languageKey="languageSelector.french" languageKey="languageSelector.french"

@ -31,7 +31,7 @@ const PlayerItemList:React.FC<MyPlayerItemListProps> =({ player, room }) => {
// const isBot = pdp === Bot; // const isBot = pdp === Bot;
let pdp; let pdp;
const isBot = player instanceof Bot; const isBot = player instanceof Bot;
isBot ? pdp = BotPDP : pdp = PersonPDP; isBot ? pdp = BotPDP : pdp = player.profilePicture;
const delBot = () => { const delBot = () => {

@ -25,7 +25,7 @@ const PlayerList: React.FC<PlayerListProps> = ({ players, playerTouched, setPlay
{ {
//@ts-ignore //@ts-ignore
players.map((player, index) => ( players.map((player, index) => (
player.id!=socket.id && <PersonStatus img={Person} state={Person} key={index} name={player.pseudo + " " + colorToEmoji(positionToColor(index), true)} playerTouched={playerTouched} setPlayerTouched={setPlayerTouched} index={index} showCircle={true}/> player.id!=socket.id && <PersonStatus img={player.profilePicture} state={Person} key={index} name={player.pseudo + " " + colorToEmoji(positionToColor(index), true)} playerTouched={playerTouched} setPlayerTouched={setPlayerTouched} index={index} showCircle={true}/>
)) ))
} }
</div> </div>

@ -0,0 +1,52 @@
import React, { useState } from 'react';
import '../Pages/Profile.css'
import dl from '../res/icon/download.png'
import defaultImg from '../res/img/Person.png'
import { useAuth } from '../Contexts/AuthContext';
//@ts-ignore
const ProfilePDP = () => {
const [selectedFile, setSelectedFile] = useState(null);
const {user} = useAuth()
// @ts-ignores
const handleFileChange = (event) => {
let file = event.target.files[0];
setSelectedFile(file);
if (file) {
const pdpUrl = URL.createObjectURL(file);
if (user!=null){
user.profilePicture = pdpUrl
}
}
};
return (
<div className='mainPDPContainer'>
{selectedFile ? (
<div >
{/* @ts-ignore */}
{/* <p>Selected File: {selectedFile.name}</p> */}
<img src={URL.createObjectURL(selectedFile)} alt="Preview" className='imgContainer' width='100px' height='100px' />
</div>
) : (
<div >
<img src={user?.profilePicture} alt="Preview" className='imgContainer' width='100px' height='100px' />
</div>
)}
<div className="parent">
<div className="file-upload">
<img src={dl} alt="upload" width='35px' height='35px'/>
{/* <h6>Cliquer ici pour ajouter une image</h6> */}
{/* <p>Taille recommandée : 100px</p> */}
<input type="file" accept="image/*" onChange={handleFileChange}/>
</div>
</div>
{/* <input type="file" accept="image/*" onChange={handleFileChange} /> */}
</div>
);
};
export default ProfilePDP;

@ -1,5 +1,7 @@
// AuthContext.js // AuthContext.js
import React, { createContext, useContext, useState, ReactNode } from 'react'; import React, { createContext, useContext, useState, ReactNode } from 'react';
import DbUserService from '../model/DataManagers/DbUserService';
import Manager from '../model/DataManagers/Manager';
import Player from '../model/Player'; import Player from '../model/Player';
import User from '../model/User'; import User from '../model/User';
import AuthService from '../services/AuthService'; import AuthService from '../services/AuthService';
@ -10,6 +12,7 @@ interface AuthContextProps {
logout: () => void; logout: () => void;
user: User | null user: User | null
setUserData: (newPlayer: User) => void setUserData: (newPlayer: User) => void
manager: Manager
} }
const AuthContext = createContext<AuthContextProps | undefined>(undefined); const AuthContext = createContext<AuthContextProps | undefined>(undefined);
@ -17,19 +20,24 @@ const AuthContext = createContext<AuthContextProps | undefined>(undefined);
const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => { const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false); const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
const [user, setUser] = useState<User| null>(null) const [user, setUser] = useState<User| null>(null)
const [manager] = useState<Manager>(new Manager(new DbUserService()))
const login = () => { const login = async () => {
setIsLoggedIn(true); setIsLoggedIn(true);
const [u, bool] = await manager.userService.fetchUserInformation()
setUser(u)
}; };
const setUserData = (player: User | null) => { const setUserData = (newPlayer: User) => {
setUser(player) setUser(newPlayer)
} }
const logout = async() => { const logout = async() => {
try { try {
await AuthService.logout(); await AuthService.logout();
setIsLoggedIn(false); setIsLoggedIn(false);
const [u, bool] = await manager.userService.fetchUserInformation()
setUser(u)
} }
catch (error) { catch (error) {
console.log(error); console.log(error);
@ -37,7 +45,7 @@ const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
}; };
return ( return (
<AuthContext.Provider value={{ isLoggedIn, login, logout, user, setUserData }}> <AuthContext.Provider value={{ isLoggedIn, login, logout, user, setUserData, manager }}>
{children} {children}
</AuthContext.Provider> </AuthContext.Provider>
); );

@ -0,0 +1,14 @@
async function loadImageAsync(url: string): Promise<string> {
try {
const response = await fetch(url);
const blob = await response.blob();
// Faire quelque chose avec le blob, par exemple, créer une URL blob
const blobUrl = URL.createObjectURL(blob);
return blobUrl
} catch (error) {
throw new Error("Erreur lors du chargement de l'image :");
}
}
export {loadImageAsync}

@ -21,7 +21,7 @@
padding: 20px; padding: 20px;
} }
.bottom{ .bottomEnd{
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
} }

@ -53,7 +53,6 @@ function EndGame() {
const {winner, person, players, indices, nbCoup, temps} =useGame() const {winner, person, players, indices, nbCoup, temps} =useGame()
console.log(winner)
let indice = indices[0] let indice = indices[0]
let losingPlayers : Player[]; let losingPlayers : Player[];
@ -63,8 +62,6 @@ function EndGame() {
indice = indices[index] indice = indices[index]
} }
if (winner != null) { if (winner != null) {
losingPlayers = players.filter(player => player.id !== winner.id); losingPlayers = players.filter(player => player.id !== winner.id);
} else { } else {
@ -76,9 +73,11 @@ function EndGame() {
} }
const theme = useTheme(); const theme = useTheme();
console.log(winner)
console.log(indices)
return ( return (
<div> <div>
{!IsSolo && {!IsSolo ? (
<div> <div>
<div className="head"> <div className="head">
<header className='leaderboard-header' style={{ borderColor: theme.colors.primary }}> <header className='leaderboard-header' style={{ borderColor: theme.colors.primary }}>
@ -90,7 +89,7 @@ function EndGame() {
<img src={Person} width='250' height='250'/> <img src={Person} width='250' height='250'/>
<h3 className='indiceDisplay'>{indices[players.findIndex((p) => p.id == winner?.id)].ToString("fr")}</h3> <h3 className='indiceDisplay'>{indices[players.findIndex((p) => p.id == winner?.id)].ToString("fr")}</h3>
</div> </div>
<div className='bottom'> <div className='bottomEnd'>
<div className='centerDivH' onClick={resetAll}> <div className='centerDivH' onClick={resetAll}>
<BigButtonNav dest="/play" img={Leave}/> <BigButtonNav dest="/play" img={Leave}/>
</div> </div>
@ -111,7 +110,8 @@ function EndGame() {
</div> </div>
</div> </div>
</div> </div>
} ): (
<div>
<div className="head"> <div className="head">
<header className='leaderboard-header' style={{ borderColor: theme.colors.primary }}> <header className='leaderboard-header' style={{ borderColor: theme.colors.primary }}>
<h1>Vous avez gagné !</h1> <h1>Vous avez gagné !</h1>
@ -120,10 +120,10 @@ function EndGame() {
</div> </div>
<div className='winner'> <div className='winner'>
<img src={Person} width='250' height='250'/> <img src={Person} width='250' height='250'/>
<h1>[ {winner?.pseudo} ]</h1> <h1>{winner?.pseudo}</h1>
</div> </div>
<div className='bottom'> <div className='bottomEnd'>
<div className='centerDivH' onClick={resetAll}> <div className='centerDivH' onClick={resetAll}>
<BigButtonNav dest="/play" img={Leave}/> <BigButtonNav dest="/play" img={Leave}/>
</div> </div>
@ -148,6 +148,8 @@ function EndGame() {
</div> </div>
</div> </div>
</div> </div>
)}
</div>
); );
} }

@ -7,28 +7,34 @@ import { useTheme } from '../Style/ThemeContext';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import ButtonImgNav from '../Components/ButtonImgNav'; import ButtonImgNav from '../Components/ButtonImgNav';
import defaultImg from "../res/img/Person.png"
import {loadImageAsync} from "../ImageHelper"
// @ts-ignore // @ts-ignore
function Home() { function Home() {
const theme=useTheme(); const theme=useTheme();
const {isLoggedIn, login} = useAuth(); const {isLoggedIn, login, user, setUserData, manager } = useAuth();
useEffect(() => { useEffect(() => {
// Verifie la connexion
const verifSession = async () => { if (user == null){
try { manager.userService.fetchUserInformation().then(([user, loggedIn]) =>{
const sessionData = await SessionService.getSession(); if (user!=null){
if (sessionData.user) { if (loggedIn){
login(); login()
setUserData(user)
} }
else{
loadImageAsync(defaultImg).then((blob) => {
user.profilePicture=blob
setUserData(user)
})
} }
catch (error) {
console.log(error);
};
} }
})
verifSession(); }
}, []); }, [isLoggedIn]);
return ( return (

@ -11,22 +11,38 @@ import PersonImg from '../res/img/Person.png';
import param from '../res/icon/param.png'; import param from '../res/icon/param.png';
import cible from '../res/icon/cible.png'; import cible from '../res/icon/cible.png';
import defaultImg from "../res/img/Person.png"
/* Component */ /* Component */
import ButtonImgNav from '../Components/ButtonImgNav'; import ButtonImgNav from '../Components/ButtonImgNav';
import { io } from 'socket.io-client';
import { Link } from 'react-router-dom';
/* Context */
import { useGame } from '../Contexts/GameContext';
import { useAuth } from '../Contexts/AuthContext';
/* Model */ /* Model */
import PersonNetwork from '../model/PersonsNetwork'; import PersonNetwork from '../model/PersonsNetwork';
import Person from '../model/Person'; import Person from '../model/Person';
import GameCreator from '../model/GameCreator'; import GameCreator from '../model/GameCreator';
import Indice from '../model/Indices/Indice'; import Indice from '../model/Indices/Indice';
import JSONParser from '../JSONParser';
import Player from '../model/Player'; import Player from '../model/Player';
import EasyBot from '../model/EasyBot'; import EasyBot from '../model/EasyBot';
import Bot from '../model/Bot'; import Bot from '../model/Bot';
import User from '../model/User'; import User from '../model/User';
import {loadImageAsync} from "../ImageHelper"
/* nav */
/* Context */ /* Context */
import { useGame } from '../Contexts/GameContext'; import { useGame } from '../Contexts/GameContext';
import { useAuth } from '../Contexts/AuthContext'; import { useNavigate } from 'react-router-dom';
/* serv */
import { socket } from "../SocketConfig";
import { io } from 'socket.io-client'; import { io } from 'socket.io-client';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
@ -35,6 +51,7 @@ import { useNavigate } from 'react-router-dom';
import { socket } from "../SocketConfig"; import { socket } from "../SocketConfig";
import { random } from 'lodash'; import { random } from 'lodash';
import SessionService from '../services/SessionService'; import SessionService from '../services/SessionService';
import { random } from 'lodash';
let gameStarted = false let gameStarted = false
@ -46,7 +63,7 @@ function Lobby() {
const { indices, setIndicesData, indice, setIndiceData, person, setPersonData, personNetwork, setPersonNetworkData, players, setPlayersData, setActualPlayerIndexData, setTurnPlayerIndexData, setRoomData } = useGame(); const { indices, setIndicesData, indice, setIndiceData, person, setPersonData, personNetwork, setPersonNetworkData, players, setPlayersData, setActualPlayerIndexData, setTurnPlayerIndexData, setRoomData } = useGame();
const {user, setUserData} = useAuth() const {user, setUserData, manager, login} = useAuth()
let first = true let first = true
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
@ -56,50 +73,28 @@ function Lobby() {
socket.emit("lobby joined", room, new EasyBot("botId" + Math.floor(Math.random() * 1000), "Bot" + Math.floor(Math.random() * 100), "").toJson()) socket.emit("lobby joined", room, new EasyBot("botId" + Math.floor(Math.random() * 1000), "Bot" + Math.floor(Math.random() * 100), "").toJson())
} }
useEffect(() => { useEffect(() => {
if (first){ if (first){
first=false first=false
if (user == null){ if (user == null){
try { manager.userService.fetchUserInformation().then(([u, loggedIn]) => {
const sessionData = SessionService.getSession(); if (u!=null){
sessionData.then((s) => { if (loggedIn){
if (s.user) { login()
// Il y a une session on récupère les infos du joueur setUserData(u)
const updatedPlayer: User = new User(socket.id, s.user.pseudo, s.user.profilePicture, {
nbGames: s.user.soloStats.nbGames,
bestScore: s.user.soloStats.bestScore,
avgNbTry: s.user.soloStats.avgNbTry,
},
{
nbGames: s.user.onlineStats.nbGames,
nbWins: s.user.onlineStats.nbWins,
ratio: s.user.onlineStats.ratio,
})
setUserData(updatedPlayer);
socket.emit("lobby joined", room, updatedPlayer.toJson())
} else {
// Pas de session on génère un guest random
const guestPlayer: User = new User(socket.id, 'Guest_' + Math.floor(Math.random() * 1000000), '',
{
nbGames: 0,
bestScore: 0,
avgNbTry: 0,
},
{
nbGames: 0,
nbWins: 0,
ratio: 0,
})
setUserData(guestPlayer);
socket.emit("lobby joined", room, guestPlayer.toJson())
} }
else{
loadImageAsync(defaultImg).then((blob) => {
u.profilePicture=blob
setUserData(u)
}) })
} }
catch (error) { socket.emit("lobby joined", room, u.toJson())
console.error(error);
} }
})
} }
else{ else{
socket.emit("lobby joined", room, user.toJson()) socket.emit("lobby joined", room, user.toJson())
@ -138,7 +133,7 @@ function Lobby() {
gameStarted = true gameStarted = true
socket.off("player left") socket.off("player left")
socket.off("new player") socket.off("new player")
navigate('/game?solo=false'); navigate('/game?solo=false&daily=false');
}); });
socket.on("new player", (tab) =>{ socket.on("new player", (tab) =>{
@ -146,6 +141,7 @@ function Lobby() {
for (const p of tab){ for (const p of tab){
tmpTab.push(JSONParser.JSONToPlayer(p)) tmpTab.push(JSONParser.JSONToPlayer(p))
} }
console.log(tmpTab)
setPlayersData(tmpTab) setPlayersData(tmpTab)
}) })

@ -30,11 +30,12 @@ const SignIn = () => {
setError(null); setError(null);
const result = await AuthService.signIn(data); const result = await AuthService.signIn(data);
// console.log(result); // console.log(result);
setShowConfirmation(true); setShowConfirmation(true);
setTimeout(() => { setTimeout(async () => {
login(); await login();
navigate('/play'); // 3 secondes avant de rediriger vers la page de connexion navigate('/play'); // 3 secondes avant de rediriger vers la page de connexion
}, 3000); }, 3000);
} }

@ -8,12 +8,9 @@ import './Play.css';
import { useTheme } from '../Style/ThemeContext'; import { useTheme } from '../Style/ThemeContext';
/* Component */ /* Component */
import ButtonImgNav from "../Components/ButtonImgNav" import ButtonImgNav from "../Components/ButtonImgNav";
import SessionService from "../services/SessionService";
/* Img */ /* Img */
import Person from '../res/img/Person.png';
/* Icon */ /* Icon */
import { socket } from '../SocketConfig'; import { socket } from '../SocketConfig';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
@ -21,19 +18,21 @@ import GameCreator from '../model/GameCreator';
import { useGame } from '../Contexts/GameContext'; import { useGame } from '../Contexts/GameContext';
import ScoreBoard from '../Components/ScoreBoard'; import ScoreBoard from '../Components/ScoreBoard';
import defaultImg from "../res/img/Person.png"
/* Types */ /* Types */
import { PlayerProps } from '../types/Player';
import Player from '../model/Player';
import Human from '../model/User';
import User from '../model/User'; import User from '../model/User';
import EnigmeDuJourCreator from '../model/EnigmeDuJourCreator'; import EnigmeDuJourCreator from '../model/EnigmeDuJourCreator';
import Stub from '../model/Stub'; import Stub from '../model/Stub';
let first = true import SessionService from '../services/SessionService';
import { loadImageAsync } from '../ImageHelper';
function Play() { function Play() {
let first = true
const theme=useTheme() const theme=useTheme()
const {isLoggedIn, login, user, setUserData } = useAuth(); const {isLoggedIn, login, user, setUserData, manager } = useAuth();
const {setDailyEnigmeData} = useGame() const {setDailyEnigmeData} = useGame()
useEffect(() => { useEffect(() => {
@ -83,6 +82,26 @@ function Play() {
const { setIndicesData, setPersonData, setPersonNetworkData } = useGame(); const { setIndicesData, setPersonData, setPersonNetworkData } = useGame();
useEffect(() => {
if (user == null){
manager.userService.fetchUserInformation().then(([user, loggedIn]) =>{
if (user!=null){
if (loggedIn){
login()
setUserData(user)
}
else{
loadImageAsync(defaultImg).then((blob) => {
user.profilePicture=blob
setUserData(user)
})
}
}
})
}
}, [isLoggedIn]);
const [room, setRoom] = useState(null); const [room, setRoom] = useState(null);
const navigate = useNavigate(); const navigate = useNavigate();
@ -90,6 +109,10 @@ function Play() {
socket.emit("lobby created") socket.emit("lobby created")
} }
useEffect(() => {
console.log(user)
}, [user])
function launchMastermind(){ function launchMastermind(){
const [networkPerson, choosenPerson, choosenIndices] = GameCreator.CreateGame(3, 30) const [networkPerson, choosenPerson, choosenIndices] = GameCreator.CreateGame(3, 30)
setPersonData(choosenPerson) setPersonData(choosenPerson)
@ -144,14 +167,14 @@ function Play() {
{/* <button className='ButtonNav'> {/* <button className='ButtonNav'>
Param Param
</button> */} </button> */}
<ButtonImgNav dest='/signup' img={Person} text="Gestion du compte"/> {/* <ButtonImgNav dest='/signup' img={defaultImg} text="Gestion du compte"/> */}
</div> </div>
<div className="MidContainer"> <div className="MidContainer">
<div> <div>
<h2> <h2>
{user && user.pseudo} {user && user.pseudo}
</h2> </h2>
<img src={Person} <img src={user?.profilePicture}
height='300' height='300'
width='300' width='300'
alt="Person" alt="Person"
@ -159,7 +182,7 @@ function Play() {
</div> </div>
<div className='buttonGroupVertical'> <div className='buttonGroupVertical'>
<button onClick={launchMastermind} className="ButtonNav" style={{backgroundColor: theme.colors.primary, borderColor: theme.colors.secondary}}> Jouer seul </button> <button onClick={launchMastermind} className="ButtonNav" style={{backgroundColor: theme.colors.primary, borderColor: theme.colors.secondary}}> Jouer seul </button>
<button onClick={launchEngimeJour} className="ButtonNav" style={{backgroundColor: theme.colors.primary, borderColor: theme.colors.secondary}}> Jouer seul mais aujourd'hui</button> <button onClick={launchEngimeJour} className="ButtonNav" style={{backgroundColor: theme.colors.primary, borderColor: theme.colors.secondary}}> Résoudre une énigme</button>
<button onClick={createLobby} className="ButtonNav" style={{backgroundColor: theme.colors.primary, borderColor: theme.colors.secondary}}> Créer une partie </button> <button onClick={createLobby} className="ButtonNav" style={{backgroundColor: theme.colors.primary, borderColor: theme.colors.secondary}}> Créer une partie </button>
<button className="ButtonNav" style={{backgroundColor: theme.colors.primary, borderColor: theme.colors.secondary}}> Rejoindre </button> <button className="ButtonNav" style={{backgroundColor: theme.colors.primary, borderColor: theme.colors.secondary}}> Rejoindre </button>

@ -0,0 +1,123 @@
.mainContainer{
display: flex;
/* flex-direction: column; */
justify-content: center;
align-items: center;
margin: 50px;
}
.mainPDPContainer{
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 2px solid whitesmoke;
border-radius: 15px;
background-color: white;
margin: 10px;
min-height: 250px;
}
/*Lpart*/
.imgContainer{
border: 5px solid black;
border-radius: 50px;
margin: 15px;
}
/*Rpart*/
.Rpart{
min-width: 40%;
min-height: 250px;
margin: 20px;
padding: 20px;
background-color: white;
border: solid 1px whitesmoke;
border-radius: 15px;
}
.username-display{
display: flex;
}
.editbutton{
border-color: white;
background-color: white;
border: none;
margin-left: 15px;
height: 25px;
width: 25px;
}
.inputpseudo{
display: 'flex';
justify-content: 'flex-start';
align-items: 'center';
flex-direction: 'row';
width: 20vw;
padding: 5;
border:none;
border-bottom: solid 2px gray;
border-radius: 5;
font-size: 40px;
}
.bottom{
display: flex;
flex-direction: column;
justify-content: end;
align-items: end;
height: 100px;
}
/*File upload*/
.parent {
/* width: 250px; */
/* margin: auto; */
margin: 2rem;
background: #ffffff;
border-radius: 25px;
/* box-shadow: 7px 20px 20px rgb(210, 227, 244); */
}
.file-upload {
text-align: center;
border: 3px dashed rgb(210, 227, 244);
/* padding: 1.5rem; */
position: relative;
cursor: pointer;
width: 100px;
height: 50px;
}
.file-upload p {
font-size: 0.5rem;
/* margin-top: 10px; */
color: #bbcada;
}
.file-upload input {
display: block;
height: 100%;
width: 100%;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
opacity: 0;
cursor: pointer;
}

@ -0,0 +1,194 @@
import React, { useEffect, useState } from 'react';
import ProfilePDP from '../Components/ProfilePDP';
import SessionService from '../services/SessionService';
import { PlayerProps } from '../types/Player';
import { delay, update } from 'lodash';
import { socket } from '../SocketConfig';
import AuthService from '../services/AuthService';
/* Style */
import './Profile.css'
import Edit from "../res/icon/edit-pen.png"
import Coche from '../res/icon/coche.png'
import Cancel from '../res/icon/cancel.png'
/* Model */
import User from '../model/User';
/* Context */
import { useAuth } from '../Contexts/AuthContext';
/* Boostrap */
import Button from 'react-bootstrap/Button';
import Alert from 'react-bootstrap/Alert';
import Modal from 'react-bootstrap/Modal';
import Form from 'react-bootstrap/Form';
import { useNavigate } from 'react-router-dom';
//@ts-ignore
const Profile = () => {
const navigate = useNavigate();
//let player;
const {user, logout} = useAuth()
// let pseudoNotNull;
// if(user?.pseudo != null){
// pseudoNotNull = user.pseudo;
// }
const [editingUsername, setEditingUsername] = useState(false);
const [newUsername, setNewUsername] = useState(user?.pseudo);
//@ts-ignore
const onUsernameChange = (newUsername) => {
if(user?.pseudo != null){
SessionService.UpdatePseudo(user.pseudo, newUsername)
user.pseudo = newUsername;
}
}
const handleUsernameChange = () => {
// Maj du pseudo
onUsernameChange(newUsername);
// Désactiver le mode d'édition
setEditingUsername(false);
};
//* Gestion Modal de suppression :
const [showDeleteModal, setShowDeleteModal] = useState(false);
const handleShowDeleteModal = () => {
setShowDeleteModal(true);
};
const handleCloseDeleteModal = () => {
setShowDeleteModal(false);
};
//* Confirmation avec la phrase :
const [confirmationPhrase, setConfirmationPhrase] = useState('');
const [showWrong, setShowWrong] = useState(false);
//@ts-ignore
const handleConfirmationPhraseChange = (e) => {
setConfirmationPhrase(e.target.value);
};
const handleDeleteAccount = () => {
// Verification de la phrase
if (confirmationPhrase.toLowerCase() === 'supprimer mon compte') {
console.log('Compte supprimé !');
console.log(user);
if(user!= null){
const pseudo = user.pseudo;
AuthService.delAccount(pseudo);
AuthService.logout();
logout();
}
else{
console.error("l'utilisateur ne peut pas être null")
}
handleCloseDeleteModal();
navigate("/play")
} else {
console.error('Phrase de confirmation incorrecte.');
setShowWrong(true);
setTimeout(async () => {
setShowWrong(false);
}, 3000);
}
};
return (
<>
<center><h1>Mon Compte</h1></center>
<div className='mainContainer'>
<div>
<ProfilePDP/>
</div>
<div className='Rpart'>
{editingUsername ? (
<div className='username-edit'>
<input
type='text'
className='inputpseudo'
value={newUsername}
onChange={(e) => setNewUsername(e.target.value)}
/>
<button className='editbutton' onClick={handleUsernameChange}>
<img src={Coche} alt='edit' width='25' height='25'/>
</button>
<button className='editbutton' onClick={() => setEditingUsername(false)}>
<img src={Cancel} alt='edit' width='25' height='25'/>
</button>
</div>
) : (
<div className='username-display'>
<h1>{user?.pseudo}</h1>
<button className='editbutton' onClick={() => setEditingUsername(true)}>
<img src={Edit} alt='edit' width='25' height='25'/>
</button>
</div>
)
}
<hr/>
{!editingUsername ? (
<Button variant="secondary">Modifier le mot de passe</Button>
) : (
<Alert key='info' variant='info' style={{width:'100%'}}>
Vous êtes en mode "édition".
</Alert>
)}
<div className='bottom'>
<>
<Button variant="danger" onClick={handleShowDeleteModal}>Supprimer</Button>
<Modal show={showDeleteModal} onHide={handleCloseDeleteModal}>
<Modal.Header closeButton>
<Modal.Title>Confirmation de suppression</Modal.Title>
</Modal.Header>
<Modal.Body>
<p>
Pour confirmer la suppression de votre compte, veuillez
entrer la phrase : "supprimer mon compte".
</p>
<Form.Control
type='text'
placeholder='Entrez la phrase de confirmation'
value={confirmationPhrase}
onChange={handleConfirmationPhraseChange}
/>
{
showWrong &&
<Alert key='infomodel' variant='danger' style={{width:'100%'}}>
La phrase de confirmation est incorrecte.
</Alert>
}
</Modal.Body>
<Modal.Footer>
<Button variant='secondary' onClick={handleCloseDeleteModal}>
Annuler
</Button>
<Button variant='danger' onClick={handleDeleteAccount}>Supprimer mon compte</Button>
</Modal.Footer>
</Modal>
</>
</div>
</div>
</div>
</>
);
};
export default Profile;

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before

Width:  |  Height:  |  Size: 254 KiB

After

Width:  |  Height:  |  Size: 254 KiB

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

@ -1,6 +1,6 @@
import { io } from "socket.io-client"; import { io } from "socket.io-client";
import { ADRESSE_WEBSERVER } from "./AdressSetup";
const socket = io(ADRESSE_WEBSERVER);
const socket = io("http://172.20.10.4:3002");
export {socket} export {socket}

@ -0,0 +1,74 @@
import SessionService from "../../services/SessionService";
import { socket } from "../../SocketConfig";
import User from "../User";
import IUserService from "./IUserService";
class DbUserService implements IUserService{
async fetchUserInformation(): Promise<[User | null, boolean]> {
try {
const sessionData = await SessionService.getSession();
// Vérifie si il y a une session
if (sessionData.user) {
// Il y a une session on récupère les infos du joueur
const updatedPlayer: User = new User(socket.id, sessionData.user.pseudo, sessionData.user.profilePicture, {
nbGames: sessionData.user.soloStats.nbGames,
bestScore: sessionData.user.soloStats.bestScore,
avgNbTry: sessionData.user.soloStats.avgNbTry,
},
{
nbGames: sessionData.user.onlineStats.nbGames,
nbWins: sessionData.user.onlineStats.nbWins,
ratio: sessionData.user.onlineStats.ratio,
})
return [updatedPlayer, true]
} else {
// Pas de session on génère un guest random
const guestPlayer: User = new User(socket.id, 'Guest_' + Math.floor(Math.random() * 1000000), '',
{
nbGames: 0,
bestScore: 0,
avgNbTry: 0,
},
{
nbGames: 0,
nbWins: 0,
ratio: 0,
})
return [guestPlayer, false]
}
} catch (error) {
console.error(error);
return [null, false]
}
}
async updateSoloStats(pseudo: string, nbGames: number, bestScore: number, avgNbTry: number): Promise<void> {
try {
const result = await SessionService.updateSoloStats(pseudo, nbGames, bestScore, avgNbTry);
if (result) {
console.log("Stats solo updated");
} else {
console.log("Stats solo not updated");
}
} catch (error) {
console.error(error);
}
}
async updateOnlineStats(pseudo: string, nbGames: number, bestScore: number, ratio: number): Promise<void> {
try {
const result = await SessionService.updateOnlineStats(pseudo, nbGames, bestScore, ratio);
if (result) {
console.log("Stats online updated");
} else {
console.log("Stats online not updated");
}
} catch (error) {
console.error(error);
}
}
}
export default DbUserService

@ -0,0 +1,10 @@
import User from "../User";
interface IUserService{
fetchUserInformation(): Promise<[User | null, boolean]>
updateSoloStats(pseudo: string, nbGames: number, bestScore: number, avgNbTry: number): Promise<void>
updateOnlineStats(pseudo: string, nbGames: number, bestScore: number, ratio: number): Promise<void>
}
export default IUserService

@ -0,0 +1,12 @@
import IUserService from "./IUserService";
class Manager{
public userService: IUserService
constructor(userService: IUserService){
this.userService = userService
}
}
export default Manager

@ -8,11 +8,15 @@ import Pair from "./Pair";
import Person from "./Person"; import Person from "./Person";
import PersonNetwork from "./PersonsNetwork"; import PersonNetwork from "./PersonsNetwork";
import Player from "./Player"; import Player from "./Player";
import DefaultImg from "../res/img/bot.png"
class EasyBot extends Bot{ class EasyBot extends Bot{
constructor(id: string, name: string, profilePicture: string){ constructor(id: string, pseudo: string, profilePicture: string){
super(id, name, profilePicture) if (profilePicture === undefined || profilePicture === ""){
profilePicture=DefaultImg
}
super(id, pseudo, profilePicture)
} }
toJson() { toJson() {

@ -1,3 +1,4 @@
abstract class Player{ abstract class Player{
public id: string public id: string
public pseudo: string; public pseudo: string;

@ -1,20 +1,22 @@
import Player from "./Player"; import Player from "./Player";
import defaultImg from "../res/img/Person.png"
class User extends Player{ class User extends Player{
public soloStats: any public soloStats: any
public onlineStats: any public onlineStats: any
constructor(id: string, name: string, profilePicture: string, soloStats: any, onlineStats: any){ constructor(id: string, pseudo: string, profilePicture: string, soloStats: any, onlineStats: any){
super(id, name, profilePicture) super(id, pseudo, profilePicture || defaultImg)
this.soloStats=soloStats this.soloStats=soloStats
this.onlineStats=onlineStats this.onlineStats=onlineStats
} }
toJson() { toJson() {
return { return {
type: "User", type: "User",
id: this.id, id: this.id,
profilePicture: this.profilePicture,
pseudo: this.pseudo, pseudo: this.pseudo,
soloStats: this.soloStats, soloStats: this.soloStats,
onlineStats: this.onlineStats onlineStats: this.onlineStats

Binary file not shown.

After

Width:  |  Height:  |  Size: 1010 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 B

@ -8,6 +8,10 @@ class AuthController {
static async signUp(req, res) { static async signUp(req, res) {
const databaseService = new DatabaseService(); const databaseService = new DatabaseService();
const pseudo = req.body.pseudo; const pseudo = req.body.pseudo;
const date = new Date();
const hour = date.getHours();
const minutes = date.getMinutes();
try { try {
await databaseService.connect(); await databaseService.connect();
@ -31,11 +35,9 @@ class AuthController {
const soloStats = await databaseService.getSoloStatsByUserId(user.idUser); const soloStats = await databaseService.getSoloStatsByUserId(user.idUser);
const onlineStats = await databaseService.getOnlineStatsByUserId(user.idUser); const onlineStats = await databaseService.getOnlineStatsByUserId(user.idUser);
console.log(soloStats);
console.log(onlineStats);
await databaseService.updateUserIDStats(user.idUser, soloStats.idSoloStats, onlineStats.idOnlineStats); await databaseService.updateUserIDStats(user.idUser, soloStats.idSoloStats, onlineStats.idOnlineStats);
// Envoyer une réponse réussie
console.log("[" + hour + ":" + minutes + "] " + user.pseudo + " have been registered.");
res.status(201).json({ message: 'Inscription réussie', user: insertedUser}); res.status(201).json({ message: 'Inscription réussie', user: insertedUser});
} }
catch (error) { catch (error) {
@ -50,6 +52,9 @@ class AuthController {
static async signIn(req, res) { static async signIn(req, res) {
const databaseService = new DatabaseService(); const databaseService = new DatabaseService();
const date = new Date();
const hour = date.getHours();
const minutes = date.getMinutes();
try{ try{
await databaseService.connect(); await databaseService.connect();
@ -70,12 +75,11 @@ class AuthController {
return; return;
} }
// Stocker l'utilisateur dans la session){ // Stocker l'utilisateur dans la session)
console.log("SESSION")
console.log(req.session);
req.session.user = user; req.session.user = user;
// Envoyer une réponse réussie // Envoyer une réponse réussie
console.log("[" + hour + ":" + minutes + "] " + user.pseudo + " have been connected.");
res.status(200).json({ message: 'Connexion réussie', user: user }); res.status(200).json({ message: 'Connexion réussie', user: user });
} }
catch(error){ catch(error){
@ -89,17 +93,45 @@ class AuthController {
} }
static async logout(req, res) { static async logout(req, res) {
const pseudo = req.session.user.pseudo;
const date = new Date();
const hour = date.getHours();
const minutes = date.getMinutes();
// Détruire la session pour déconnecter l'utilisateur // Détruire la session pour déconnecter l'utilisateur
req.session.destroy((err) => { req.session.destroy((err) => {
if (err) { if (err) {
console.error(err); console.error(err);
res.status(500).json({ error: 'Erreur lors de la déconnexion.' }); res.status(500).json({ error: 'Erreur lors de la déconnexion.' });
} else { } else {
console.log("[" + hour + ":" + minutes + "] " + pseudo + " have been disconnected.");
res.status(200).json({ message: 'Déconnexion réussie' }); res.status(200).json({ message: 'Déconnexion réussie' });
} }
}); });
} }
static async delAccount(req, res){
const db = new DatabaseService();
try{
await db.connect();
const user = await db.getUserByPseudo(req.body.pseudo);
if(!user){
res.status(400).json({ error: 'Le pseudo n\'existe pas.' });
return;
}
await db.deleteSoloStat(user.idUser);
await db.deleteOnlineStat(user.idUser);
await db.deleteUser(user.idUser);
}
catch(error){
console.error(error);
res.status(500).json({ error: 'Erreur lors de la supression du compte.' });
}
finally{
db.disconnect();
}
}
} }
module.exports = AuthController; module.exports = AuthController;

@ -3,6 +3,9 @@ const DatabaseService = require('../services/DatabaseService');
class SessionController { class SessionController {
static async getUserInformation(req, res) { static async getUserInformation(req, res) {
const db = new DatabaseService(); const db = new DatabaseService();
const date = new Date();
const hour = date.getHours();
const minutes = date.getMinutes();
try{ try{
await db.connect(); await db.connect();
@ -15,8 +18,7 @@ class SessionController {
req.session.user.soloStats = await db.getSoloStatsByUserId(req.session.user.idUser); req.session.user.soloStats = await db.getSoloStatsByUserId(req.session.user.idUser);
req.session.user.onlineStats = await db.getOnlineStatsByUserId(req.session.user.idUser); req.session.user.onlineStats = await db.getOnlineStatsByUserId(req.session.user.idUser);
console.log(req.session.user); console.log("[" + hour + ":" + minutes + "] " + req.session.user.pseudo + " have a session.");
res.status(200).json({ user: req.session.user }); res.status(200).json({ user: req.session.user });
} }
catch(error){ catch(error){
@ -27,6 +29,118 @@ class SessionController {
await db.disconnect(); await db.disconnect();
} }
} }
static async updateSoloStats(req, res) {
const db = new DatabaseService();
const date = new Date();
const hour = date.getHours();
const minutes = date.getMinutes();
try{
await db.connect();
const user = await db.getUserByPseudo(req.body.pseudo);
if (!user) {
res.status(200).json({ error: "true", message: 'User not found' });
return;
}
const soloStats = await db.getSoloStatsByUserId(user.idUser);
if (!soloStats) {
res.status(200).json({ error: "true", message: 'Solo stats not found' });
return;
}
await db.updateSoloStats(user.idUser, req.body.nbGames, req.body.bestScore, req.body.avgNbTry);
const newSoloStats = await db.getSoloStatsByUserId(user.idUser);
req.session.user.soloStats = newSoloStats;
console.log("[" + hour + ":" + minutes + "] " + req.session.user.pseudo + "'s solot_stats are updated.");
res.status(200).json({ user: req.session.user });
}
catch(error){
console.error(error);
res.status(500).json({ error: 'Erreur lors de la mise à jour des statistiques solo.' });
}
finally{
await db.disconnect();
}
}
static async updateOnlineStats(req, res) {
const db = new DatabaseService();
const date = new Date();
const hour = date.getHours();
const minutes = date.getMinutes();
try{
await db.connect();
const user = await db.getUserByPseudo(req.body.pseudo);
if (!user) {
res.status(200).json({ error: "true", message: 'User not found' });
return;
}
const onlineStats = await db.getOnlineStatsByUserId(user.idUser);
if (!onlineStats) {
res.status(200).json({ error: "true", message: 'Online stats not found' });
return;
}
await db.updateOnlineStats(user.idUser, req.body.nbGames, req.body.nbWins, req.body.ratio);
const newOnlineStats = await db.getOnlineStatsByUserId(user.idUser);
req.session.user.onlineStats = newOnlineStats;
console.log("[" + hour + ":" + minutes + "] " + req.session.user.pseudo + "'s online_stats are updated.");
res.status(200).json({ user: req.session.user });
}
catch(error){
console.error(error);
res.status(500).json({ error: 'Erreur lors de la mise à jour des statistiques en ligne.' });
}
finally{
await db.disconnect();
}
}
static async UpdatePseudo(req, res){
const db = new DatabaseService();
try{
await db.connect();
const user = await db.getUserByPseudo(req.body.pseudo);
console.log("utilisateur" + user.idUser + " pseudo" + user.pseudo)
if (!user) {
res.status(200).json({ error: "true", message: 'User not found' });
return;
}
await db.updatePseudo(user.idUser, req.body.newPseudo); //* update
const updatedUser = await db.getUserByPseudo(req.body.newPseudo);
console.log("updaetdutilisateur" + updatedUser.idUser + " pseudo" + updatedUser.pseudo)
req.session.user.pseudo = updatedUser.pseudo;
console.log("req.session.user.pseudo" + req.session.user.pseudo)
res.status(200).json({ user: req.session.user }); //verif rep
}
catch(error){
console.error(error);
res.status(500).json({ error: 'Erreur lors de la modification du pseudo de l\'utilisateur.' });
}
finally{
await db.disconnect();
}
}
} }
module.exports = SessionController; module.exports = SessionController;

@ -7,8 +7,12 @@ const SessionController = require('../controllers/SessionController');
router.post('/auth/signup', AuthController.signUp); router.post('/auth/signup', AuthController.signUp);
router.post('/auth/signin', AuthController.signIn); router.post('/auth/signin', AuthController.signIn);
router.delete('/auth/logout', AuthController.logout) router.delete('/auth/logout', AuthController.logout)
router.delete('/auth/delAccount', AuthController.delAccount)
// Routes pour les sessions // Routes pour les sessions
router.get('/session', SessionController.getUserInformation); router.get('/session', SessionController.getUserInformation);
router.put('/session/updatePseudo', SessionController.UpdatePseudo);
router.put('/session/updateSoloStats', SessionController.updateSoloStats);
router.put('/session/updateOnlineStats', SessionController.updateOnlineStats);
module.exports = router; module.exports = router;

@ -6,13 +6,14 @@ const crypto = require('crypto');
const authRoutes = require('./routes/authRoutes'); const authRoutes = require('./routes/authRoutes');
const DatabaseService = require('./services/DatabaseService'); const DatabaseService = require('./services/DatabaseService');
const app = express(); const app = express();
const port = 3003; const port = 3003;
// Middleware // Middleware
app.use(cors( app.use(cors(
{ {
origin: ['http://localhost:3000', "http://172.20.10.4:3000"], origin: ["http://localhost:3000", "http://172.20.10.4:3000"],
credentials: true credentials: true
} }
)); // Autoriser les requêtes cross-origin )); // Autoriser les requêtes cross-origin

@ -1,5 +1,6 @@
const sqlite3 = require('sqlite3'); const sqlite3 = require('sqlite3');
const path = require('path'); const path = require('path');
const { rejects } = require('assert');
class DatabaseService { class DatabaseService {
constructor(){ constructor(){
@ -50,6 +51,20 @@ class DatabaseService {
}); });
} }
// Récupère l'utilisateur par son id
async getUserByID(id){
return new Promise((resolve, reject) => {
this.client.get('SELECT * FROM users WHERE idUser = ?', id, (err, result) => {
if(err){
reject(err);
}
else{
resolve(result);
}
});
});
}
// Récupère stats solo de l'utilisateur // Récupère stats solo de l'utilisateur
async getSoloStatsByUserId(userId){ async getSoloStatsByUserId(userId){
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -106,6 +121,33 @@ class DatabaseService {
}); });
} }
// Mettre à jour les stats solo de l'utilisateur
async updateSoloStats(userId, nbGames, bestScore, avgNbTry){
return new Promise((resolve, reject) => {
this.client.run('UPDATE solo_stats SET nbGames = ?, bestScore = ?, avgNbTry = ? WHERE idUser = ?', [nbGames, bestScore, avgNbTry, userId], (err, result) => {
if(err){
reject(err);
}
else{
resolve(result);
}
});
});
}
// Mettre à jour les stats online de l'utilisateur
async updateOnlineStats(userId, nbGames, nbWins, ratio){
return new Promise((resolve, reject) => {
this.client.run('UPDATE online_stats SET nbGames = ?, nbWins = ?, ratio = ? WHERE idUser = ?', [nbGames, nbWins, ratio, userId], (err, result) => {
if(err){
reject(err);
}
else{
resolve(result);
}
});
});
}
async initSoloStats(userId) { async initSoloStats(userId) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -132,6 +174,58 @@ class DatabaseService {
}); });
}); });
} }
async deleteUser(userId){
return new Promise((resolve, reject) => {
this.client.run('DELETE FROM users WHERE idUser=?', userId, (err, result) => {
if(err){
reject(err);
}
else{
resolve(result);
}
});
});
}
async deleteSoloStat(userId){
return new Promise((resolve, reject) => {
this.client.run('DELETE FROM solo_stats WHERE idUser=?', userId, (err, result) => {
if(err){
reject(err);
}
else{
resolve(result);
}
});
});
}
async deleteOnlineStat(userId){
return new Promise((resolve, reject) => {
this.client.run('DELETE FROM online_stats WHERE idUser=?', userId, (err, result) => {
if(err){
reject(err);
}
else{
resolve(result);
}
});
});
}
async updatePseudo(userId, newPseudo){
return new Promise((resolve, reject) => {
this.client.run('UPDATE users SET pseudo = ? WHERE idUser = ?', newPseudo, userId, (err, result) => {
if(err){
reject(err);
}
else{
resolve(result);
}
});
});
}
} }
module.exports = DatabaseService; module.exports = DatabaseService;

@ -1,4 +1,7 @@
import VerificationService from './VerificationService'; import VerificationService from './VerificationService';
import {ADRESSE_DBSERVER} from "../AdressSetup"
import User from '../model/User';
class AuthService{ class AuthService{
// Méthode pour vérifier les données de connexion // Méthode pour vérifier les données de connexion
@ -12,7 +15,7 @@ class AuthService{
static async signUp(data: any) { static async signUp(data: any) {
try { try {
const response = await fetch('http://172.20.10.4:3003/auth/signup', { const response = await fetch(ADRESSE_DBSERVER + '/auth/signup', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -36,7 +39,7 @@ class AuthService{
static async signIn(data: any) { static async signIn(data: any) {
try { try {
const response = await fetch('http://172.20.10.4:3003/auth/signin', { const response = await fetch(ADRESSE_DBSERVER + '/auth/signin', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -61,11 +64,35 @@ class AuthService{
static async logout() { static async logout() {
try { try {
const response = await fetch('http://172.20.10.4:3003/auth/logout', { const response = await fetch(ADRESSE_DBSERVER + '/auth/logout', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
});
if (response.ok) {
const result = await response.json();
return result;
} else {
const errorResponse = await response.json();
throw new Error(errorResponse.error);
}
} catch (error) {
console.error(error);
throw error;
}
}
static async delAccount(pseudo: string){
try {
const response = await fetch(ADRESSE_DBSERVER + '/auth/delAccount', {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ pseudo }),
credentials: 'include', credentials: 'include',
}); });

@ -1,7 +1,9 @@
import {ADRESSE_DBSERVER} from "../AdressSetup"
class SessionService { class SessionService {
static async getSession() { static async getSession() {
try { try {
const response = await fetch('http://172.20.10.4:3003/session', { const response = await fetch(ADRESSE_DBSERVER + '/session', {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -21,7 +23,94 @@ class SessionService {
throw error; throw error;
} }
} }
static async updateSoloStats(pseudo: string, nbGames: number, bestScore: number, avgNbTry: number){
try {
const response = await fetch(ADRESSE_DBSERVER + '/session/updateSoloStats', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
pseudo,
nbGames,
bestScore,
avgNbTry
}),
});
if (response.ok) {
const result = await response.json();
return result;
} else {
const errorResponse = await response.json();
throw new Error(errorResponse.error);
}
} catch (error) {
console.error(error);
throw error;
}
} }
export default SessionService; static async updateOnlineStats(pseudo: string, nbGames: number, nbWins: number, ratio: number){
try {
console.log("updateOnlineStats : ", pseudo, nbGames, nbWins, ratio);
const response = await fetch(ADRESSE_DBSERVER + '/session/updateOnlineStats', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
pseudo,
nbGames,
nbWins,
ratio
}),
});
if (response.ok) {
const result = await response.json();
return result;
} else {
const errorResponse = await response.json();
throw new Error(errorResponse.error);
}
} catch (error) {
console.error(error);
throw error;
}
}
static async UpdatePseudo(pseudo : string, newPseudo : string) {
console.log("pseudo : " + pseudo + " newpseudo : " + newPseudo)
try {
const response = await fetch(ADRESSE_DBSERVER + '/session/updatePseudo', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
pseudo,
newPseudo
}),
credentials: 'include',
});
if (response.ok) {
const result = await response.json();
return result;
} else {
const errorResponse = await response.json();
throw new Error(errorResponse.error);
}
} catch (error) {
console.error(error);
throw error;
}
}
}
export default SessionService;

@ -0,0 +1,17 @@
#!/bin/sh
node server/server.js &
node src/server/server.js
if lsof -Pi :3002 -sTCP:LISTEN -t >/dev/null; then
# Tuer le processus associé au port
pid=$(lsof -Pi :3002 -sTCP:LISTEN -t)
kill -9 $pid
fi
if lsof -Pi :3003 -sTCP:LISTEN -t >/dev/null; then
# Tuer le processus associé au port
pid=$(lsof -Pi :3003 -sTCP:LISTEN -t)
kill -9 $pid
fi

File diff suppressed because it is too large Load Diff

@ -1,3 +0,0 @@
\relax
\@writefile{toc}{\contentsline {paragraph}{Première énigme}{2}{}\protected@file@percent }
\gdef \@abspage@last{2}

Binary file not shown.

@ -1,132 +0,0 @@
\documentclass[11pt]{article}
\usepackage{fullpage}
\usepackage{times}
\usepackage{tikz}
\usepackage{paralist}
\usetikzlibrary {shapes.multipart}
\newcommand{\Basketball}{\includegraphics[width=.5cm]{ballon-de-basket.png}}
\newcommand{\Football}{\includegraphics[width=.4cm]{ballon-de-foot.png}}
\newcommand{\Bowling}{\includegraphics[width=.5cm]{bowling.png}}
\newcommand{\Baseball}{\includegraphics[width=.5cm]{baseball.png}}
\newcommand{\Tennis}{\includegraphics[width=.5cm]{tennis.png}}
\begin{document}
\thispagestyle{empty}
Voici le graphe de SocialGraphe
\begin{center}
\begin{tikzpicture}[scale=.9]
\node[draw, circle split] (0) at (0,0) { Alexander \nodepart{lower} \Football{} \Bowling{}};
\node[draw, circle split] (1) at (4,0) { Wyatt \nodepart{lower} \Baseball{} \Tennis{}};
\node[draw, circle split] (2) at (8,0) { Mia \nodepart{lower} \Basketball{}};
\node[draw, circle split] (3) at (12,0) { William \nodepart{lower} \Baseball{} \Football{}};
\node[draw, circle split] (4) at (16,0) { Zoey \nodepart{lower} \Basketball{} \Bowling{} \Tennis{}};
\node[draw, circle split] (5) at (0,4) { Isabella \nodepart{lower} \Tennis{}};
\node[draw, circle split] (6) at (4,4) { Abigail \nodepart{lower} \Baseball{}};
\node[draw, circle split] (7) at (8,4) { Savannah \nodepart{lower} \Bowling{} \Basketball{} \Football{}};
\node[draw, circle split] (8) at (12,4) { Peyton \nodepart{lower} \Football{}};
\node[draw, circle split] (9) at (16,4) { Alice \nodepart{lower} \Tennis{} \Baseball{}};
\node[draw, circle split] (10) at (0,8) { Sophia \nodepart{lower} \Bowling{} \Basketball{} \Bowling{}};
\node[draw, circle split] (11) at (4,8) { Layla \nodepart{lower} \Tennis{} \Baseball{} \Football{}};
\node[draw, circle split] (12) at (8,8) { Ava \nodepart{lower} \Basketball{}};
\node[draw, circle split] (13) at (12,8) { Harper \nodepart{lower} \Bowling{}};
\node[draw, circle split] (14) at (16,8) { Sebastian \nodepart{lower} \Tennis{} \Basketball{} \Baseball{}};
\node[draw, circle split] (15) at (0,12) { Michael \nodepart{lower} \Football{}};
\node[draw, circle split] (16) at (4,12) { Natalie \nodepart{lower} \Bowling{} \Football{} \Baseball{}};
\node[draw, circle split] (17) at (8,12) { Penelope \nodepart{lower} \Basketball{}};
\node[draw, circle split] (18) at (12,12) { Lily \nodepart{lower} \Tennis{} \Tennis{}};
\node[draw, circle split] (19) at (16,12) { Eleanor \nodepart{lower} \Football{}};
\node[draw, circle split] (20) at (0,16) { Henry \nodepart{lower} \Bowling{} \Basketball{}};
\node[draw, circle split] (21) at (4,16) { Claire \nodepart{lower} \Baseball{} \Basketball{}};
\node[draw, circle split] (22) at (8,16) { Caleb \nodepart{lower} \Baseball{}};
\node[draw, circle split] (23) at (12,16) { Charlotte \nodepart{lower} \Bowling{} \Football{} \Tennis{}};
\node[draw, circle split] (24) at (16,16) { Luke \nodepart{lower} \Football{}};
\node[draw, circle split] (25) at (0,20) { Connor \nodepart{lower} \Baseball{} \Tennis{}};
\node[draw, circle split] (26) at (4,20) { Aiden \nodepart{lower} \Basketball{} \Bowling{} \Tennis{}};
\node[draw, circle split] (27) at (8,20) { Aurora \nodepart{lower} \Football{}};
\node[draw, circle split] (28) at (12,20) { Nathan \nodepart{lower} \Bowling{} \Baseball{}};
\node[draw, circle split] (29) at (16,20) { Aurora \nodepart{lower} \Basketball{}};
\draw (0) -- (11);
\draw (0) -- (13);
\draw (0) -- (18);
\draw (1) -- (13);
\draw (1) -- (24);
\draw (2) -- (22);
\draw (2) -- (16);
\draw (2) -- (9);
\draw (2) -- (6);
\draw (3) -- (4);
\draw (3) -- (20);
\draw (4) -- (28);
\draw (4) -- (3);
\draw (5) -- (17);
\draw (5) -- (15);
\draw (5) -- (24);
\draw (6) -- (2);
\draw (7) -- (17);
\draw (7) -- (24);
\draw (7) -- (22);
\draw (7) -- (11);
\draw (8) -- (25);
\draw (8) -- (21);
\draw (8) -- (24);
\draw (8) -- (11);
\draw (9) -- (2);
\draw (10) -- (25);
\draw (10) -- (26);
\draw (10) -- (27);
\draw (11) -- (0);
\draw (11) -- (7);
\draw (11) -- (8);
\draw (12) -- (20);
\draw (12) -- (27);
\draw (13) -- (0);
\draw (13) -- (1);
\draw (14) -- (15);
\draw (15) -- (5);
\draw (15) -- (14);
\draw (15) -- (20);
\draw (15) -- (17);
\draw (16) -- (2);
\draw (16) -- (26);
\draw (17) -- (5);
\draw (17) -- (7);
\draw (17) -- (15);
\draw (17) -- (20);
\draw (18) -- (0);
\draw (19) -- (23);
\draw (20) -- (3);
\draw (20) -- (12);
\draw (20) -- (15);
\draw (20) -- (17);
\draw (21) -- (8);
\draw (22) -- (2);
\draw (22) -- (7);
\draw (22) -- (23);
\draw (23) -- (19);
\draw (23) -- (22);
\draw (24) -- (1);
\draw (24) -- (5);
\draw (24) -- (7);
\draw (24) -- (8);
\draw (25) -- (8);
\draw (25) -- (10);
\draw (26) -- (10);
\draw (26) -- (16);
\draw (26) -- (29);
\draw (27) -- (10);
\draw (27) -- (12);
\draw (28) -- (4);
\draw (29) -- (26);
\end{tikzpicture}
\end{center}
\paragraph{Première énigme}
Trouver qui est le coupable avec les indices suivants.
\begin{compactitem}
\item Indice 1 : Le suspect pratique au moins du Baseball et/ou du Basketball .
\item Indice 2 : Le suspect pratique 2 ou 1 sport(s).
\item Indice 3 : Le suspect a les cheveux Roux ou Blond .
\item Indice 4 : Le suspect a au moins un ami avec les cheveux Roux .
\end{compactitem}
% Solution : Nathan
\end{document}
Loading…
Cancel
Save