diff --git a/docs/Memorama.postman_collection[1].json b/docs/Memorama.postman_collection[1].json new file mode 100644 index 0000000..4d8c244 --- /dev/null +++ b/docs/Memorama.postman_collection[1].json @@ -0,0 +1,254 @@ +{ + "info": { + "_postman_id": "2083381c-545f-49b5-859b-46b1336c927a", + "name": "Memorama", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "29780256" + }, + "item": [ + { + "name": "crear partida", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"nombre profesor\" : \"Surisadai\",\r\n \"nombre juego\" : \"TPSI 8A\",\r\n \"cartas\" : {\r\n \"0\" : {\r\n \"question\" : \"a\",\r\n \"answer\" : \"a\"\r\n },\r\n \"1\" : {\r\n \"question\" : \"b\",\r\n \"answer\" : \"b\"\r\n },\r\n \"3\" : {\r\n \"question\" : \"c\",\r\n \"answer\" : \"c\"\r\n },\r\n \"4\" : {\r\n \"question\" : \"d\",\r\n \"answer\" : \"d\"\r\n }\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://127.0.0.1:8000/api/crearPartida/", + "protocol": "http", + "host": [ + "127", + "0", + "0", + "1" + ], + "port": "8000", + "path": [ + "api", + "crearPartida", + "" + ] + } + }, + "response": [] + }, + { + "name": "unirse", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "partidaID", + "value": "1", + "type": "text" + }, + { + "key": "nombre alumno", + "value": "milo", + "type": "text" + } + ] + }, + "url": { + "raw": "http://127.0.0.1:8000/api/unirse/", + "protocol": "http", + "host": [ + "127", + "0", + "0", + "1" + ], + "port": "8000", + "path": [ + "api", + "unirse", + "" + ] + } + }, + "response": [] + }, + { + "name": "cartas subpartida", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://127.0.0.1:8000/api/cartas/subpartida/:subpartida/?alumno=1", + "protocol": "http", + "host": [ + "127", + "0", + "0", + "1" + ], + "port": "8000", + "path": [ + "api", + "cartas", + "subpartida", + ":subpartida", + "" + ], + "query": [ + { + "key": "alumno", + "value": "1" + } + ], + "variable": [ + { + "key": "subpartida", + "value": "1" + } + ] + } + }, + "response": [] + }, + { + "name": "alumnos subpartida", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://127.0.0.1:8000/api/subpartida/alumnos/:subpartida/", + "protocol": "http", + "host": [ + "127", + "0", + "0", + "1" + ], + "port": "8000", + "path": [ + "api", + "subpartida", + "alumnos", + ":subpartida", + "" + ], + "variable": [ + { + "key": "subpartida", + "value": "1" + } + ] + } + }, + "response": [] + }, + { + "name": "cartas partida", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://127.0.0.1:8000/api/partida/:partida/", + "protocol": "http", + "host": [ + "127", + "0", + "0", + "1" + ], + "port": "8000", + "path": [ + "api", + "partida", + ":partida", + "" + ], + "variable": [ + { + "key": "partida", + "value": "1" + } + ] + } + }, + "response": [] + }, + { + "name": "voltear cartas", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"alumnoID\": 1,\r\n \"carta1ID\": 1,\r\n \"carta2ID\": 3\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://127.0.0.1:8000/api/subpartida/:subpartida/", + "protocol": "http", + "host": [ + "127", + "0", + "0", + "1" + ], + "port": "8000", + "path": [ + "api", + "subpartida", + ":subpartida", + "" + ], + "variable": [ + { + "key": "subpartida", + "value": "1" + } + ] + } + }, + "response": [] + }, + { + "name": "update score", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://127.0.0.1:8000/api/updateScore/:alumno/", + "protocol": "http", + "host": [ + "127", + "0", + "0", + "1" + ], + "port": "8000", + "path": [ + "api", + "updateScore", + ":alumno", + "" + ], + "variable": [ + { + "key": "alumno", + "value": "1" + } + ] + } + }, + "response": [] + } + ] +} \ No newline at end of file diff --git a/frontend/src/app/iniciar/page.jsx b/frontend/src/app/iniciar/page.jsx index bbf3c58..ea50d8f 100644 --- a/frontend/src/app/iniciar/page.jsx +++ b/frontend/src/app/iniciar/page.jsx @@ -1,23 +1,27 @@ -import HostHeader from "@/components/host/HostHeader" -import MainButton from "@/components/MainButton" -import styles from '@/styles/pages/Iniciar.module.css' +'use client'; +import useGameInfo from "@/libs/helpers/host/useGameInfo"; -import { DummyGame, DummySubroom } from "@/libs/Dummy" - -/* - * Dummy Data - */ -const GameInfo = DummyGame -const { players : a } = DummySubroom -const players = [ ...a, ...a] +import styles from '@/styles/pages/Iniciar.module.css'; +import HostHeader from "@/components/host/HostHeader"; +import MainButton from "@/components/MainButton"; export default function page() { + const { gameInfo, players, statusOptions, cancelGame } = useGameInfo(); + return (
- - - + + + + cancelGame( gameInfo.gameID ) } + />
@@ -25,8 +29,12 @@ export default function page() {

Jugadores en linea

- { players.map( ( player, key ) =>

{player.name}

) } + { players.map( ( player, key ) =>

{player.nombre}

) }
+ { + gameInfo.playersOnline == 0 && +

No hay jugadores en lĂ­nea

+ }
diff --git a/frontend/src/app/stats/page.jsx b/frontend/src/app/stats/page.jsx index ccab465..f9877cc 100644 --- a/frontend/src/app/stats/page.jsx +++ b/frontend/src/app/stats/page.jsx @@ -1,32 +1,22 @@ "use client" +import useGameInfo from "@/libs/helpers/host/useGameInfo" +import useSubRoomLogic from "@/libs/helpers/host/useSubRoomLogic" + import style from "@/styles/pages/stats.module.css" import MainButton from "@/components/MainButton" import SubroomStat from "@/components/host/SubroomStat" import HostHeader from "@/components/host/HostHeader" -import { DummyGame, DummySubroom } from "@/libs/Dummy"; - const Page = () => { - /* - * I'm working with dummy data for now - * This will be replaced with the real-time connection to the server - * - * We have to get the Subrooms from the server and then map them to the SubroomStat component - * The game info probably from cookies - * - Name of the game - * - Game ID - * - Number of players - * - Teacher name - */ - const GameInfo = DummyGame - const Subrooms = [DummySubroom, DummySubroom] + const { gameInfo, players, statusOptions } = useGameInfo(); + const { subrooms } = useSubRoomLogic( players ); return (
- - + + @@ -38,8 +28,8 @@ const Page = () => { - {Subrooms.map(subroom => ( - + { Object.keys( subrooms ).map(subroom => ( + ))} diff --git a/frontend/src/components/Inicio.jsx b/frontend/src/components/Inicio.jsx index f1accdf..e0207f6 100644 --- a/frontend/src/components/Inicio.jsx +++ b/frontend/src/components/Inicio.jsx @@ -22,7 +22,15 @@ export default function Inicio() { partidaID: gameCode, "nombre alumno": name, }); - console.log(res) + + if ( !res ) { + alert("Error al unirse al juego"); + } + + if (res) { + console.log("res", res); + redirect(`/espera/?gameCode=${ gameCode.toString() }`); + } }; return ( diff --git a/frontend/src/components/host/GameForm.jsx b/frontend/src/components/host/GameForm.jsx index 847c0e8..76f49e3 100644 --- a/frontend/src/components/host/GameForm.jsx +++ b/frontend/src/components/host/GameForm.jsx @@ -36,23 +36,33 @@ const GameForm = () => { setCards(updatedCards); }; - const startGame = (userName, gameName, cards) => { + const startGame = async (userName, gameName, cards) => { let cardsObject = {} for (let i = 0; i < cards.length; i++) { cardsObject[i] = cards[i]; } - console.log(cardsObject) - console.log(cards) - - console.log("Exporting Game Data:", userName, gameName, cardsObject); - let res = CreateGame({ + let res = await CreateGame({ "nombre profesor": userName, "nombre juego": gameName, cartas: cardsObject, }); - // router.push("/iniciar"); + if (!res) { + alert("Error al crear la partida"); + return; + } + + if ( res ) { + console.log(res);//{profesorID: 9, partidaID: 9} + + const query = new URLSearchParams({ + profesorID: res.profesorID.toString(), + partidaID: res.partidaID.toString(), + }).toString(); + + router.push(`/iniciar?${query}`); + } }; const exportGameData = () => { diff --git a/frontend/src/components/host/SubroomStat.jsx b/frontend/src/components/host/SubroomStat.jsx index f2a3e67..18ee37e 100644 --- a/frontend/src/components/host/SubroomStat.jsx +++ b/frontend/src/components/host/SubroomStat.jsx @@ -3,7 +3,7 @@ import SubroomPlayer from '@/components/host/SubroomPlayer' export default function SubroomStat( { subroomID, score, position, players = [] } ) { - const MaxSubRoomScore = Math.max(...players.map( player => player.score ), 0) + const MaxPlayersScore = Math.max(...players.map( player => player.score ), 0) return ( @@ -19,9 +19,9 @@ export default function SubroomStat( { subroomID, score, position, players = [] {players.map( (player, key) => ( ))} diff --git a/frontend/src/libs/helpers/host/useGameInfo.js b/frontend/src/libs/helpers/host/useGameInfo.js new file mode 100644 index 0000000..a813d15 --- /dev/null +++ b/frontend/src/libs/helpers/host/useGameInfo.js @@ -0,0 +1,199 @@ +import { useSearchParams, useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; + +import { getStudentsGame } from "@/services/GetAlumnosPartida"; +import { getPartida } from "@/services/getPatida"; +import { updatePartida } from "@/services/updatePartida"; + +export default function useGameInfo() { + const searchParams = useSearchParams(); + const router = useRouter(); + + const [ queryParams, setQueryParams] = useState({ profesorID: 0, partidaID: 0 }); + const [ gameInfo, setGameInfo ] = useState({ gameName : '', gameID : 0, teacherName : '', playersOnline : 0, estado : 'Iniciando' }); + const [ players, setPlayers ] = useState([]); + const [ statusOptions, setStatusOptions ] = useState({}); + + const startGame = async ( queryParams ) => { + const query = new URLSearchParams( queryParams ).toString(); + const res = await updatePartida( queryParams.partidaID, { estado: 'en-progreso' } ); + + if ( !res ) { + ERROR_MESSAGE_HANDLER( 'starting game', res ); + return; + } + + router.push(`/stats?${query}`); + } + + const endGame = async ( queryParams ) => { + const query = new URLSearchParams( queryParams ).toString(); + const res = await updatePartida( queryParams.partidaID , { estado: 'finalizado' } ); + + if ( !res ) { + ERROR_MESSAGE_HANDLER( 'ending game', res ); + return; + } + + router.push(`/iniciar?${query}`); + } + + const cancelGame = async ( id ) => { + const res = await updatePartida( id, { estado: 'cancelado' } ); + + if ( !res ) { + ERROR_MESSAGE_HANDLER( 'canceling game', res ); + return; + } + + router.push(`/`); + } + + const statusHandler = () => { + /** + * I pass the queryParams to the startGame and endGame functions + * because the functions aren't reading the last value of the queryParams + * they read { profesorID: 0, partidaID: 0 } instead of the current value + */ + const currentStatus = gameInfo.estado; + + switch ( currentStatus ) { + case 'Iniciando': + setStatusOptions({ + estado: currentStatus, + action: () => startGame( queryParams ), + cta : 'Iniciar juego' + }); + break; + + case 'en-progreso': + setStatusOptions({ + estado: currentStatus, + action: () => endGame( queryParams ), + cta : 'Finalizar juego' + }); + break; + + case 'finalizado': + setStatusOptions({ + estado: currentStatus, + action: () => startGame( queryParams ), + cta : 'Jugar de nuevo' + }); + break; + + default: + setStatusOptions({ + estado: 'Iniciando', + action: () => startGame( queryParams ), + cta : 'Iniciar juego' + }); + break; + } + }; + + /* + * Get the query params + */ + useEffect(() => { + const profesorID = searchParams.get("profesorID"); + const partidaID = searchParams.get("partidaID"); + + // if there's no profesorID or partidaID, return + if ( !profesorID || !partidaID ) return; + + // if one of the values is different from the current queryParams, update it + if ( parseInt( profesorID ) != queryParams.profesorID || parseInt( partidaID ) != queryParams.partidaID ) { + setQueryParams({ + profesorID: parseInt( profesorID ), + partidaID: parseInt( partidaID ) + }); + } + + }, [ searchParams ]); + + /* + * Set the gameID + */ + useEffect(() => { + setGameInfo({ ...gameInfo, gameID: parseInt( queryParams.partidaID ) }); + }, [ queryParams ]); + + /* + * Get the game info + */ + useEffect(() => { + const getGameInfo = async () => { + const game = await getPartida( queryParams.partidaID ); + + if ( game ) { + setGameInfo({ + gameName: game.nombre, + gameID: queryParams.partidaID, + teacherName: game.profesorID,// This should be the teacher name + estado: game.estado + }); + } + else { + ERROR_MESSAGE_HANDLER( 'getting game info', game ); + } + } + + if ( queryParams.partidaID ) getGameInfo(); + }, [ queryParams ]); + + /* + * Get online players + */ + useEffect(() => { + const getOnlinePlayers = async () => { + const gameStudents = await getStudentsGame( queryParams.partidaID ); + + if ( gameStudents ) setPlayers( gameStudents ); + else ERROR_MESSAGE_HANDLER( 'No se encontraron alumnos', gameStudents ); + } + + if ( queryParams.partidaID ) { + getOnlinePlayers(); + + const intervalId = setInterval(() => { + getOnlinePlayers(); + }, 3000); // Fetch every 3 seconds + + return () => clearInterval(intervalId); // Clear interval on unmount + } + + }, [ queryParams ]); + + /* + * Update the players online count + */ + useEffect(() => { + setGameInfo({ ...gameInfo, playersOnline: players.length }); + }, [ players ]); + + /* + * Update the status options + */ + useEffect(() => { + statusHandler(); + }, [ gameInfo.estado, queryParams ]); + + return { + queryParams, + gameInfo, + players, + statusOptions, + cancelGame + } +} + +const ERROR_MESSAGE = ( action, err ) => ` +Error while ${ action }: + +${ err } +`; + +const ERROR_MESSAGE_HANDLER = ( err ) => { + console.log( ERROR_MESSAGE( err ) ); +} \ No newline at end of file diff --git a/frontend/src/libs/helpers/host/useSubRoomLogic.js b/frontend/src/libs/helpers/host/useSubRoomLogic.js new file mode 100644 index 0000000..ec32728 --- /dev/null +++ b/frontend/src/libs/helpers/host/useSubRoomLogic.js @@ -0,0 +1,85 @@ +import { useEffect, useState } from "react" + +export default function useSubRoomLogic( inputPlayers ) { + const [ players, setPlayers ] = useState( inputPlayers ); + const [ subrooms, setSubrooms ] = useState({}); + + useEffect(() => { + if ( inputPlayers.length != players.length ) { + const sortedPlayers = sortPlayers( inputPlayers, subrooms ); + + // if there are new players, update the subrooms + if ( sortedPlayers ) { + setSubrooms( sortedPlayers ); + setPlayers( inputPlayers ); + } + } + }, [ inputPlayers ]); + + useEffect(() => { + // calculate the score of the subrooms + // and update the subrooms + const updatedSubrooms = calculateSubroomScore( subrooms ); + setSubrooms( updatedSubrooms ); + }, [ players ]); + + return { + subrooms + } +} + +const sortPlayers = ( players, currentSubrooms ) => { + let subrooms = { ...currentSubrooms }; + + /* + * get the new players by comparing the players with the subrooms + * if the player is not in any subroom, add it to a new subroom + */ + const newPlayers = players.filter(player => + !Object.values(subrooms).some(subroom => { + /* + * check if the player is in the subroom + * It'd be nice if we could use a player ID + * So we could compare the players solo by ID + */ + return subroom.players.some( subroomPlayer => ( + subroomPlayer.nombre == player.nombre + && subroomPlayer.subpartidaID == player.subpartidaID + && subroomPlayer.puntaje == player.puntaje + ) + ); + } + ) + ); + + newPlayers.forEach( newPlayer => { + const subroomID = newPlayer.subpartidaID; + + // if the subroom doesn't exist, create it + if (!subrooms[ subroomID ]) { + subrooms[ subroomID ] = { players: [], subroomID, score: 0, position: 0 }; + } + + // add the player to the subroom + subrooms[ subroomID ].players.push( newPlayer ); + }); + + if ( newPlayers.length > 0 ) { + return subrooms; + } +} + +const calculateSubroomScore = ( subrooms ) => { + let updatedSubrooms = { ...subrooms }; + + Object.keys( updatedSubrooms ).forEach( subroomID => { + const subroom = updatedSubrooms[ subroomID ]; + /* + * Calculate the score of the subroom by adding the score of each player + */ + const score = subroom.players.reduce( (acc, player) => acc + player.puntaje, 0 ); + updatedSubrooms[ subroomID ].score = score; + }); + + return updatedSubrooms; +} \ No newline at end of file diff --git a/frontend/src/services/GetAlumnosPartida.js b/frontend/src/services/GetAlumnosPartida.js index 08ebdd8..25839c6 100644 --- a/frontend/src/services/GetAlumnosPartida.js +++ b/frontend/src/services/GetAlumnosPartida.js @@ -1,16 +1,19 @@ -export const getStudentsSubGame = async (partidaID) => { +import { SERVER_URL } from "./SETTINGS"; + +export const getStudentsGame = async (partidaID) => { const res = await fetch( - `http://127.0.0.1:8000/api/partida/alumnos/${partidaID}/`, + `${ SERVER_URL }/api/partida/alumnos/${partidaID}/`, { method: "GET", headers: { "Content-Type": "application/json" }, - body: {}, } ); if (!res.ok) { - console.log(res); - throw new Error(res); + console.log(res) + // In case of error, return undefined + // This will be handled in the component + return undefined; } return res.json(); diff --git a/frontend/src/services/GetAlumnosSubpartida.js b/frontend/src/services/GetAlumnosSubpartida.js index 6354a22..1bda6ee 100644 --- a/frontend/src/services/GetAlumnosSubpartida.js +++ b/frontend/src/services/GetAlumnosSubpartida.js @@ -1,4 +1,4 @@ -export const getStudentsGame = async (subpartidaID) => { +export const getStudentsSubGame = async (subpartidaID) => { const res = await fetch( `http://127.0.0.1:8000/api/subpartida/alumnos/${subpartidaID}/`, { diff --git a/frontend/src/services/SETTINGS.js b/frontend/src/services/SETTINGS.js new file mode 100644 index 0000000..5277a51 --- /dev/null +++ b/frontend/src/services/SETTINGS.js @@ -0,0 +1,5 @@ +export const SERVER_PROTOCOL = 'http' +export const SERVER_PORT = '8000' +export const SERVER_IP = '127.0.0.1' + +export const SERVER_URL = `${SERVER_PROTOCOL}://${SERVER_IP}:${SERVER_PORT}` \ No newline at end of file diff --git a/frontend/src/services/crearPartida.js b/frontend/src/services/crearPartida.js index 64c8ac5..d7e932f 100644 --- a/frontend/src/services/crearPartida.js +++ b/frontend/src/services/crearPartida.js @@ -1,13 +1,17 @@ +import { SERVER_URL } from "./SETTINGS"; + export const CreateGame = async (data) => { - const res = await fetch("http://127.0.0.1:8000/api/crear/", { + const res = await fetch(`${ SERVER_URL }/api/crear/`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); if (!res.ok) { - console.log(res); - throw new Error(res); + console.log(res) + // In case of error, return undefined + // This will be handled in the component + return undefined; } return res.json(); diff --git a/frontend/src/services/getPatida.js b/frontend/src/services/getPatida.js new file mode 100644 index 0000000..c8d97ab --- /dev/null +++ b/frontend/src/services/getPatida.js @@ -0,0 +1,18 @@ +import { SERVER_URL } from "./SETTINGS"; + +export const getPartida = async ( idPartida ) => { + const res = await fetch(`${ SERVER_URL }/api/partida/${ idPartida }`, { + method: "GET", + headers: { "Content-Type": "application/json" }, + }); + + if (!res.ok) { + console.log(res) + // In case of error, return undefined + // This will be handled in the component + return undefined; + } + + return res.json(); + }; + \ No newline at end of file diff --git a/frontend/src/services/updatePartida.js b/frontend/src/services/updatePartida.js new file mode 100644 index 0000000..7f6b557 --- /dev/null +++ b/frontend/src/services/updatePartida.js @@ -0,0 +1,21 @@ +import { SERVER_URL } from "./SETTINGS"; + +export const updatePartida = async (partidaID, data) => { + const res = await fetch( + `${ SERVER_URL }/api/update/partida/${partidaID}/`, + { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data) + } + ); + + if (!res.ok) { + console.log(res) + // In case of error, return undefined + // This will be handled in the component + return undefined; + } + + return res.json(); +} \ No newline at end of file