diff --git a/.gitignore b/.gitignore index 74524a9..253262c 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,8 @@ bower_components psd thumb -sketch \ No newline at end of file +sketch + +### modules ### +yarn.lock +package-lock.json \ No newline at end of file diff --git a/cryptide_project/src/App.tsx b/cryptide_project/src/App.tsx index ac0525d..f6a601f 100644 --- a/cryptide_project/src/App.tsx +++ b/cryptide_project/src/App.tsx @@ -10,6 +10,7 @@ import Home from './Pages/Home'; import Login from './Pages/LoginForm'; import SignUp from './Pages/SignUpForm'; import Play from './Pages/Play'; +import Profile from './Pages/Profile'; import Lobby from './Pages/Lobby'; import InGame from './Pages/InGame'; @@ -84,6 +85,7 @@ function App() { } /> }/> } /> + } /> {/* }/> */} diff --git a/cryptide_project/src/Components/GraphContainer.tsx b/cryptide_project/src/Components/GraphContainer.tsx index 901cf57..87d9ade 100644 --- a/cryptide_project/src/Components/GraphContainer.tsx +++ b/cryptide_project/src/Components/GraphContainer.tsx @@ -53,7 +53,6 @@ let cptTour: number = 0 const navigate = useNavigate(); const [lastIndex, setLastIndex] = useState(-1) - useEffect(() =>{ touchedPlayer=playerTouched if (touchedPlayer == -1){ @@ -481,6 +480,7 @@ let cptTour: number = 0 cptHistory = 0 askedWrong=false askedWrongBot=false + socket.off("end game") socket.off("asked all") diff --git a/cryptide_project/src/Components/NavBar.tsx b/cryptide_project/src/Components/NavBar.tsx index 4500c85..3118e17 100644 --- a/cryptide_project/src/Components/NavBar.tsx +++ b/cryptide_project/src/Components/NavBar.tsx @@ -23,16 +23,31 @@ import './NavBar.css'; /* Style */ import { useTheme } from '../Style/ThemeContext'; import { useAuth } from '../Contexts/AuthContext'; +import { useNavigate } from 'react-router-dom'; // @ts-ignore function AppNavbar({changeLocale}) { 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 ( - + logo @@ -54,7 +69,7 @@ function AppNavbar({changeLocale}) { align="end" drop='down-centered' > - Profil + Profil =({ player, room }) => { // const isBot = pdp === Bot; let pdp; const isBot = player instanceof Bot; - isBot ? pdp = BotPDP : pdp = PersonPDP; + isBot ? pdp = BotPDP : pdp = player.profilePicture; const delBot = () => { diff --git a/cryptide_project/src/Components/ProfilePDP.tsx b/cryptide_project/src/Components/ProfilePDP.tsx new file mode 100644 index 0000000..7395740 --- /dev/null +++ b/cryptide_project/src/Components/ProfilePDP.tsx @@ -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 ( +
+ {selectedFile ? ( +
+ {/* @ts-ignore */} + {/*

Selected File: {selectedFile.name}

*/} + Preview +
+ ) : ( +
+ Preview +
+ )} +
+
+ upload + {/*
Cliquer ici pour ajouter une image
*/} +

Taille recommandée : 100px

+ +
+
+ {/* */} +
+ ); +}; + +export default ProfilePDP; diff --git a/cryptide_project/src/Contexts/AuthContext.tsx b/cryptide_project/src/Contexts/AuthContext.tsx index a061775..e0f6f1a 100644 --- a/cryptide_project/src/Contexts/AuthContext.tsx +++ b/cryptide_project/src/Contexts/AuthContext.tsx @@ -1,5 +1,7 @@ // AuthContext.js 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 User from '../model/User'; import AuthService from '../services/AuthService'; @@ -10,6 +12,7 @@ interface AuthContextProps { logout: () => void; user: User | null setUserData: (newPlayer: User) => void + manager: Manager } const AuthContext = createContext(undefined); @@ -17,19 +20,24 @@ const AuthContext = createContext(undefined); const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => { const [isLoggedIn, setIsLoggedIn] = useState(false); const [user, setUser] = useState(null) + const [manager] = useState(new Manager(new DbUserService())) - const login = () => { + const login = async () => { setIsLoggedIn(true); + const [u, bool] = await manager.userService.fetchUserInformation() + setUser(u) }; - const setUserData = (player: User | null) => { - setUser(player) + const setUserData = (newPlayer: User) => { + setUser(newPlayer) } const logout = async() => { try { await AuthService.logout(); setIsLoggedIn(false); + const [u, bool] = await manager.userService.fetchUserInformation() + setUser(u) } catch (error) { console.log(error); @@ -37,7 +45,7 @@ const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => { }; return ( - + {children} ); diff --git a/cryptide_project/src/Pages/Home.tsx b/cryptide_project/src/Pages/Home.tsx index 249099c..1c16b9e 100644 --- a/cryptide_project/src/Pages/Home.tsx +++ b/cryptide_project/src/Pages/Home.tsx @@ -11,24 +11,22 @@ import ButtonImgNav from '../Components/ButtonImgNav'; // @ts-ignore function Home() { const theme=useTheme(); - const {isLoggedIn, login} = useAuth(); + const {isLoggedIn, login, user, setUserData, manager } = useAuth(); useEffect(() => { - // Verifie la connexion - const verifSession = async () => { - try { - const sessionData = await SessionService.getSession(); - if (sessionData.user) { - login(); + + if (user == null){ + manager.userService.fetchUserInformation().then(([user, loggedIn]) =>{ + if (user!=null){ + setUserData(user) + if (loggedIn){ + login() + } + console.log('isLoggedIn : ', isLoggedIn); } - } - catch (error) { - console.log(error); - }; + }) } - - verifSession(); - }, []); + }, [isLoggedIn]); return ( diff --git a/cryptide_project/src/Pages/Lobby.tsx b/cryptide_project/src/Pages/Lobby.tsx index 5f86ed5..5c4b5df 100644 --- a/cryptide_project/src/Pages/Lobby.tsx +++ b/cryptide_project/src/Pages/Lobby.tsx @@ -38,7 +38,7 @@ function Lobby() { 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 const params = new URLSearchParams(window.location.search); @@ -53,45 +53,15 @@ function Lobby() { first=false if (user == null){ - try { - const sessionData = SessionService.getSession(); - sessionData.then((s) => { - if (s.user) { - // Il y a une session on récupère les infos du joueur - 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()) - + manager.userService.fetchUserInformation().then(([u, loggedIn]) => { + if (u!=null){ + setUserData(u) + if (loggedIn){ + login() } - }) - } - catch (error) { - console.error(error); - } + socket.emit("lobby joined", room, u.toJson()) + } + }) } else{ socket.emit("lobby joined", room, user.toJson()) diff --git a/cryptide_project/src/Pages/LoginForm.tsx b/cryptide_project/src/Pages/LoginForm.tsx index 0c56b55..9800f65 100644 --- a/cryptide_project/src/Pages/LoginForm.tsx +++ b/cryptide_project/src/Pages/LoginForm.tsx @@ -30,11 +30,12 @@ const SignIn = () => { setError(null); const result = await AuthService.signIn(data); + // console.log(result); setShowConfirmation(true); - setTimeout(() => { - login(); + setTimeout(async () => { + await login(); navigate('/play'); // 3 secondes avant de rediriger vers la page de connexion }, 3000); } diff --git a/cryptide_project/src/Pages/Play.tsx b/cryptide_project/src/Pages/Play.tsx index 257177a..dcc9361 100644 --- a/cryptide_project/src/Pages/Play.tsx +++ b/cryptide_project/src/Pages/Play.tsx @@ -26,53 +26,8 @@ import User from '../model/User'; function Play() { const theme=useTheme() - const {isLoggedIn, login, user, setUserData } = useAuth(); - - useEffect(() => { - const fetchUserInformation = async () => { - 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, - }) - login(); - setUserData(updatedPlayer); - } 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); - } - } catch (error) { - console.error(error); - } - }; - - console.log('isLoggedIn : ', isLoggedIn); - fetchUserInformation(); - }, [isLoggedIn]); + const {user} = useAuth(); - const { setIndicesData, setPersonData, setPersonNetworkData } = useGame(); @@ -83,6 +38,10 @@ function Play() { socket.emit("lobby created") } + useEffect(() => { + console.log(user) + }, [user]) + function launchMastermind(){ const [networkPerson, choosenPerson, choosenIndices] = GameCreator.CreateGame(3, 30) setPersonData(choosenPerson) @@ -96,7 +55,7 @@ function Play() { useEffect(() => { const handleLobbyCreated = (newRoom: any) => { - setRoom(newRoom); + setRoom(newRoom); }; // Ajouter l'event listener @@ -130,7 +89,7 @@ function Play() {

{user && user.pseudo}

- Person { + + //let player; + const {user} = useAuth() + + //! useeffect pour l'instant, il faudra voir pour changer la facons de prendre une session + + return ( +
+ +

{user?.pseudo}

+
+ ); +}; + +export default Profile; diff --git a/ballon-de-basket.png b/cryptide_project/src/Script/ballon-de-basket.png similarity index 100% rename from ballon-de-basket.png rename to cryptide_project/src/Script/ballon-de-basket.png diff --git a/ballon-de-foot.png b/cryptide_project/src/Script/ballon-de-foot.png similarity index 100% rename from ballon-de-foot.png rename to cryptide_project/src/Script/ballon-de-foot.png diff --git a/baseball.png b/cryptide_project/src/Script/baseball.png similarity index 100% rename from baseball.png rename to cryptide_project/src/Script/baseball.png diff --git a/bowling.png b/cryptide_project/src/Script/bowling.png similarity index 100% rename from bowling.png rename to cryptide_project/src/Script/bowling.png diff --git a/tennis.png b/cryptide_project/src/Script/tennis.png similarity index 100% rename from tennis.png rename to cryptide_project/src/Script/tennis.png diff --git a/cryptide_project/src/model/DataManagers/DbUserService.ts b/cryptide_project/src/model/DataManagers/DbUserService.ts new file mode 100644 index 0000000..49dded6 --- /dev/null +++ b/cryptide_project/src/model/DataManagers/DbUserService.ts @@ -0,0 +1,49 @@ +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] + } + } + +} + +export default DbUserService \ No newline at end of file diff --git a/cryptide_project/src/model/DataManagers/IUserService.ts b/cryptide_project/src/model/DataManagers/IUserService.ts new file mode 100644 index 0000000..5eb0f43 --- /dev/null +++ b/cryptide_project/src/model/DataManagers/IUserService.ts @@ -0,0 +1,9 @@ +import User from "../User"; + +interface IUserService{ + + fetchUserInformation(): Promise<[User | null, boolean]> +} + + +export default IUserService \ No newline at end of file diff --git a/cryptide_project/src/model/DataManagers/Manager.ts b/cryptide_project/src/model/DataManagers/Manager.ts new file mode 100644 index 0000000..a6caeb6 --- /dev/null +++ b/cryptide_project/src/model/DataManagers/Manager.ts @@ -0,0 +1,12 @@ +import IUserService from "./IUserService"; + +class Manager{ + + public userService: IUserService + + constructor(userService: IUserService){ + this.userService = userService + } +} + +export default Manager \ No newline at end of file diff --git a/cryptide_project/src/model/Player.ts b/cryptide_project/src/model/Player.ts index bc85a1d..c7a7786 100644 --- a/cryptide_project/src/model/Player.ts +++ b/cryptide_project/src/model/Player.ts @@ -1,3 +1,4 @@ + abstract class Player{ public id: string public pseudo: string; diff --git a/cryptide_project/src/model/User.tsx b/cryptide_project/src/model/User.tsx index ee287a8..66573d8 100644 --- a/cryptide_project/src/model/User.tsx +++ b/cryptide_project/src/model/User.tsx @@ -1,15 +1,20 @@ import Player from "./Player"; +import defaultpdp from "../res/img/Person.png" class User extends Player{ public soloStats: any public onlineStats: any - constructor(id: string, name: string, profilePicture: string, soloStats: any, onlineStats: any){ - super(id, name, profilePicture) + constructor(id: string, pseudo: string, profilePicture: string, soloStats: any, onlineStats: any){ + if (profilePicture == ""){ + profilePicture = defaultpdp + } + super(id, pseudo, profilePicture) this.soloStats=soloStats this.onlineStats=onlineStats } + toJson() { return { diff --git a/cryptide_project/src/server/db/socialgraph.db b/cryptide_project/src/server/db/socialgraph.db index cfc4f11..d53b462 100644 Binary files a/cryptide_project/src/server/db/socialgraph.db and b/cryptide_project/src/server/db/socialgraph.db differ diff --git a/graph.aux b/graph.aux deleted file mode 100644 index cde4834..0000000 --- a/graph.aux +++ /dev/null @@ -1,3 +0,0 @@ -\relax -\@writefile{toc}{\contentsline {paragraph}{Première énigme}{2}{}\protected@file@percent } -\gdef \@abspage@last{2} diff --git a/graph.pdf b/graph.pdf deleted file mode 100644 index 242fe3b..0000000 Binary files a/graph.pdf and /dev/null differ diff --git a/graph.tex b/graph.tex deleted file mode 100644 index eaa16b7..0000000 --- a/graph.tex +++ /dev/null @@ -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}