diff --git a/.gitignore b/.gitignore index 5f31eec7..836b7f0e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,9 @@ coverage docs/build -# Dependencies lock files -package-lock.json - # Node.js dependencies node_modules # IDE/Editor files .idea/ .vscode/ - -# Environment variables -.env diff --git a/gatewayservice/gateway-service.js b/gatewayservice/gateway-service.js index 32949f90..c79ea1d8 100644 --- a/gatewayservice/gateway-service.js +++ b/gatewayservice/gateway-service.js @@ -63,6 +63,85 @@ app.get('/questions', async (req, res) => { } }); +app.post('/user/edit', async (req, res) => { + try { + // Forward the add user request to the user service + const userResponse = await axios.post(userServiceUrl + '/user/edit', req.body); + res.json(userResponse.data); + } catch (error) { + if (error.response && error.response.status) { + res.status(error.response.status).json({ error: error.response.data.error }); + } else if (error.message) { + res.status(500).json({ error: error.message }); + } else { + res.status(500).json({ error: 'Internal Server Error' }); + } + } +}); + +app.get('/group/list', async (req, res) => { + try { + const userResponse = await axios.get(userServiceUrl + '/group/api/list'); + res.json(userResponse.data); + } catch (error) { + if (error.response && error.response.status) { + res.status(error.response.status).json({ error: error.response.data.error }); + } else if (error.message) { + res.status(500).json({ error: error.message }); + } else { + res.status(500).json({ error: 'Internal Server Error' }); + } + } +}); + + +app.post('/group/add', async (req, res) => { + try { + const userResponse = await axios.post(userServiceUrl + '/group/add', req.body); + res.json(userResponse.data); + } catch (error) { + if (error.response && error.response.status) { + res.status(error.response.status).json({ error: error.response.data.error }); + } else if (error.message) { + res.status(500).json({ error: error.message }); + } else { + res.status(500).json({ error: 'Internal Server Error' }); + } + } +}); + +app.get('/group/:name', async (req, res) => { + try { + const { name } = req.params; + const userResponse = await axios.get(`${userServiceUrl}/group/${name}`); + res.json(userResponse.data); + } catch (error) { + if (error.response && error.response.status) { + res.status(error.response.status).json({ error: error.response.data.error }); + } else if (error.message) { + res.status(500).json({ error: error.message }); + } else { + res.status(500).json({ error: 'Internal Server Error' }); + } + } +}); + +app.post('/group/:name/join', async (req, res) => { + try { + const { name } = req.params; + const userResponse = await axios.post(`${userServiceUrl}/group/${name}/join`, req.body); + res.json(userResponse.data); + } catch (error) { + if (error.response && error.response.status) { + res.status(error.response.status).json({ error: error.response.data.error }); + } else if (error.message) { + res.status(500).json({ error: error.message }); + } else { + res.status(500).json({ error: 'Internal Server Error' }); + } + } +}); + // Start the gateway service const server = app.listen(port, () => { console.log(`Gateway Service listening at http://localhost:${port}`); diff --git a/users/index.js b/users/index.js index df43b4a1..3a8d2a58 100644 --- a/users/index.js +++ b/users/index.js @@ -1,9 +1,11 @@ // Imports (express syntax) const express = require('express'); + // Routes: const authRoutes = require('./routes/auth-routes.js'); const userRoutes = require('./routes/user-routes.js'); +const groupRoutes = require('./routes/group-routes.js'); // App definition and const app = express(); @@ -15,6 +17,7 @@ app.use(express.json()); // Routes middlewares to be used app.use('/user', userRoutes); app.use('/login', authRoutes); +app.use('/group', groupRoutes); // Start the service const server = app.listen(port, () => { diff --git a/users/models/user-model.js b/users/models/user-model.js index 3fc86750..0ffa40a2 100644 --- a/users/models/user-model.js +++ b/users/models/user-model.js @@ -1,5 +1,6 @@ const { Sequelize, DataTypes } = require('sequelize'); + // Database connection configuration const sequelize = new Sequelize({ host: 'mariadb', @@ -15,6 +16,7 @@ const User = sequelize.define('User', { username: { type: DataTypes.STRING, primaryKey: true, + notEmpty: true, }, password: { type: DataTypes.STRING, @@ -23,10 +25,12 @@ const User = sequelize.define('User', { name: { type: DataTypes.STRING, allowNull: false, + notEmpty: true, }, surname: { type: DataTypes.STRING, allowNull: false, + notEmpty: true, }, createdAt: { type: DataTypes.DATE, @@ -63,10 +67,14 @@ const Group = sequelize.define('Group', { type: DataTypes.STRING, primaryKey: true }, + creator: { + type: DataTypes.STRING, + }, createdAt: { type: DataTypes.DATE, defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') } + // When the session is introduced, the creator user and more stuff will be added }) const UserGroup = sequelize.define('UserGroup', { diff --git a/users/package-lock.json b/users/package-lock.json index 58362157..827f228b 100644 --- a/users/package-lock.json +++ b/users/package-lock.json @@ -11,7 +11,10 @@ "dependencies": { "bcrypt": "^5.1.1", "body-parser": "^1.20.2", + "dotenv": "^16.4.5", "express": "^4.18.2", + "express-session": "^1.18.0", + "jose": "5.2.2", "jsonwebtoken": "^9.0.2", "mariadb": "^2.5.1", "sequelize": "^6.6.5" @@ -2019,6 +2022,17 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dottie": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", @@ -2229,6 +2243,37 @@ "node": ">= 0.10.0" } }, + "node_modules/express-session": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz", + "integrity": "sha512-m93QLWr0ju+rOwApSsyso838LQwgfs44QtOP/WBiwtAgPIo/SAh1a5c6nn2BR6mFNZehTpqKDESzP+fRHVbxwQ==", + "dependencies": { + "cookie": "0.6.0", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" + }, "node_modules/express/node_modules/body-parser": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", @@ -3469,6 +3514,14 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jose": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.2.2.tgz", + "integrity": "sha512-/WByRr4jDcsKlvMd1dRJnPfS1GVO3WuKyaurJ/vvXcOaUQO8rnNObCQMlv/5uCceVQIq5Q4WLF44ohsdiTohdg==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4001,6 +4054,14 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4271,6 +4332,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -4988,6 +5057,17 @@ "node": ">= 0.6" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", diff --git a/users/package.json b/users/package.json index e193cfed..17a06b3c 100644 --- a/users/package.json +++ b/users/package.json @@ -20,13 +20,16 @@ "dependencies": { "bcrypt": "^5.1.1", "body-parser": "^1.20.2", + "dotenv": "^16.4.5", "express": "^4.18.2", - "sequelize": "^6.6.5", + "express-session": "^1.18.0", + "jose": "5.2.2", "jsonwebtoken": "^9.0.2", - "mariadb": "^2.5.1" + "mariadb": "^2.5.1", + "sequelize": "^6.6.5" }, "devDependencies": { "jest": "^29.7.0", "supertest": "^6.3.4" } -} \ No newline at end of file +} diff --git a/users/routes/auth-routes.js b/users/routes/auth-routes.js index b6a3ce07..92fffd53 100644 --- a/users/routes/auth-routes.js +++ b/users/routes/auth-routes.js @@ -4,9 +4,12 @@ const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); const { User } = require('../models/user-model'); + +require('dotenv').config(); + router.post('/', async (req, res) => { try { - + const { username, password } = req.body; // Check if required fields are present in the request body @@ -20,10 +23,28 @@ router.post('/', async (req, res) => { // Check if the user exists and verify the password if (user && user.username === username && await bcrypt.compare(password, user.password)) { - // Generate a JWT token - const token = jwt.sign({ userId: user.id }, 'your-secret-key', { expiresIn: '1h' }); + + // Token payload + const payload = { + userId: username + }; + + //CHANGE THIS TO ENVIRONMENT VARS (NOT PUBLIC) + const secretKey = 'eyJhbGciOiJIUzI1NiJ9.eyJSb2xlIjoiQWRtaW4iLCJJc3N1ZXIiOiJJc3N1ZXIiLCJVc2VybmFtZSI6IkphdmFJblVzZSIsImV4cCI6MTcwOTQ2OTkzMywiaWF0IjoxNzA5NDY5OTMzfQ.pQ8H6FKeZyEHPnGs4Ah3-n-QXJ5E8YM_u1AfZHI7Ip0'; + + const options = { + expiresIn: '1h' + }; + + //Token sign and creation + const token = jwt.sign(payload, secretKey, options); + + //This should save token in user's browser, it doesn't seem to do anything + res.cookie("token", token); // maxAge (millis) = 1 hour + // Respond with the token and user information - return res.json({ token, username, createdAt: user.createdAt }); + return res.status(200).json({ token, username, createdAt: user.createdAt }); + } else { return res.status(401).json({ error: 'Invalid credentials' }); } diff --git a/users/routes/group-routes.js b/users/routes/group-routes.js new file mode 100644 index 00000000..e025f8a8 --- /dev/null +++ b/users/routes/group-routes.js @@ -0,0 +1,49 @@ +const express = require('express'); +const router = express.Router(); +const bcrypt = require('bcrypt'); +const { Group,User,UserGroup } = require('../models/user-model'); + +//Group internal routes +const apiRoutes = require('../services/group-api'); + +// Adding a group to the database +router.post('/add', async (req, res) => { + try { + const { name,username } = req.body; + + const newGroup = await Group.create({ + name: name, + creator: username, + createdAt: new Date() + }); + + res.json(newGroup); + } catch (error) { + return res.status(500).json({ error: 'Internal Server Error' }); + } +}); + +// Adding a new relationship in the database between a group and a user when this one joins it +router.post('/:name/join', async (req, res) => { + try { + const groupName = req.params.name; + const { username } = req.body; + + // Need to be tested + const newUserGroup = await UserGroup.create({ + name: groupName, + username: username, + createdAt: new Date() + }); + + res.json(newUserGroup); + } catch (error) { + return res.status(500).json({ error: 'Internal Server Error' }); + } +}); + + +//Api middleware +router.use('/api', apiRoutes); + +module.exports = router; \ No newline at end of file diff --git a/users/routes/user-routes.js b/users/routes/user-routes.js index ad798377..3c6787a5 100644 --- a/users/routes/user-routes.js +++ b/users/routes/user-routes.js @@ -52,6 +52,7 @@ router.post('/add', async (req, res) => { // Route for edit a user router.post('/edit', async (req, res) => { try { + const { username, total_score, correctly_answered_questions, incorrectly_answered_questions, total_time_played, games_played } = req.body; // Find the user in the database by their username @@ -67,11 +68,11 @@ router.post('/edit', async (req, res) => { } // Update the user's fields with the provided values - userToUpdate.total_score = total_score; - userToUpdate.correctly_answered_questions = correctly_answered_questions; - userToUpdate.incorrectly_answered_questions = incorrectly_answered_questions; - userToUpdate.total_time_played = total_time_played; - userToUpdate.games_played = games_played; + userToUpdate.total_score = userToUpdate.total_score + total_score; + userToUpdate.correctly_answered_questions = userToUpdate.correctly_answered_questions + correctly_answered_questions; + userToUpdate.incorrectly_answered_questions = userToUpdate.incorrectly_answered_questions + incorrectly_answered_questions; + userToUpdate.total_time_played = userToUpdate.total_time_played + total_time_played; + userToUpdate.games_played = userToUpdate.games_played + games_played; // Save the changes to the database await userToUpdate.save(); diff --git a/users/services/authVerifyMiddleWare b/users/services/authVerifyMiddleWare new file mode 100644 index 00000000..82d8fe5a --- /dev/null +++ b/users/services/authVerifyMiddleWare @@ -0,0 +1,23 @@ +// middleware/authMiddleware.js + +const jwt = require('jsonwebtoken'); + +function verifyToken(token) { + + + if (token === null) { + return res.status(401).json({ message: "Token not provided" }); + } + + try { + const payload = jwt.verify(token, 'eyJhbGciOiJIUzI1NiJ9.eyJSb2xlIjoiQWRtaW4iLCJJc3N1ZXIiOiJJc3N1ZXIiLCJVc2VybmFtZSI6IkphdmFJblVzZSIsImV4cCI6MTcwOTQ2OTkzMywiaWF0IjoxNzA5NDY5OTMzfQ.pQ8H6FKeZyEHPnGs4Ah3-n-QXJ5E8YM_u1AfZHI7Ip0'); + + //Once the payload is obtained, you can get user info using its keys + req.username = payload.userId; + + } catch (error) { + return res.status(403).json({ message: "Token not valid" }); + } + } + +module.exports = verifyToken; \ No newline at end of file diff --git a/users/services/group-api.js b/users/services/group-api.js new file mode 100644 index 00000000..c02ebc7f --- /dev/null +++ b/users/services/group-api.js @@ -0,0 +1,59 @@ +const express = require('express'); +const router = express.Router(); +const { Group,User,UserGroup } = require('../models/user-model'); + + +// Getting the list of groups in the database +router.get('/list', async (req, res) => { + try { + const allGroups = await Group.findAll(); + const groupsJSON = allGroups.map(group => group.toJSON()); + + const allGroupsJSON = { + groups: groupsJSON + }; + + res.json(allGroupsJSON); + } catch (error) { + return res.status(500).json({ error: 'Internal Server Error' }); + } +}); + + + +// Getting a group by its name +router.get('/:name', async (req, res) => { + try { + const groupName = req.params.name; + + // Need also to get the group members + const group = await Group.findOne({ + where: { + name: groupName + } + }); + if (!group) { + return res.status(404).json({ error: 'Group not found' }); + } + + const groupUsers = await User.findAll({ + include: [ + { + model: UserGroup, + where: { name: groupName } + } + ] + }); + + // Construct JSON response + const groupJSON = group.toJSON(); + groupJSON.users = groupUsers.map(user => user.toJSON()); + + res.json(groupJSON); + } catch (error) { + return res.status(400).json({ error: error.message }); + } +}); + + +module.exports = router; \ No newline at end of file diff --git a/users/services/user-api.js b/users/services/user-api.js index 159fe98b..77b5c96f 100644 --- a/users/services/user-api.js +++ b/users/services/user-api.js @@ -74,20 +74,4 @@ router.get('/ranking', async (req,res) => { }); - - -//Get Groups - -//Get group by name - - - - - - - - - - - module.exports = router; \ No newline at end of file diff --git a/webapp/public/defaultFavicon.ico b/webapp/public/defaultFavicon.ico new file mode 100644 index 00000000..a11777cc Binary files /dev/null and b/webapp/public/defaultFavicon.ico differ diff --git a/webapp/public/favicon.ico b/webapp/public/favicon.ico index a11777cc..7ffc7f65 100644 Binary files a/webapp/public/favicon.ico and b/webapp/public/favicon.ico differ diff --git a/webapp/public/gameImg/foto0.jpg b/webapp/public/gameImg/foto0.jpg deleted file mode 100644 index a5a33487..00000000 Binary files a/webapp/public/gameImg/foto0.jpg and /dev/null differ diff --git a/webapp/public/gameImg/foto3.jpg b/webapp/public/gameImg/foto3.jpg index a5a33487..f9a1b837 100644 Binary files a/webapp/public/gameImg/foto3.jpg and b/webapp/public/gameImg/foto3.jpg differ diff --git a/webapp/public/gameImg/foto4.jpg b/webapp/public/gameImg/foto4.jpg index a5a33487..e213a172 100644 Binary files a/webapp/public/gameImg/foto4.jpg and b/webapp/public/gameImg/foto4.jpg differ diff --git a/webapp/public/gameImg/foto5.jpg b/webapp/public/gameImg/foto5.jpg deleted file mode 100644 index f9a1b837..00000000 Binary files a/webapp/public/gameImg/foto5.jpg and /dev/null differ diff --git a/webapp/public/gameImg/foto6.jpg b/webapp/public/gameImg/foto6.jpg deleted file mode 100644 index a5a33487..00000000 Binary files a/webapp/public/gameImg/foto6.jpg and /dev/null differ diff --git a/webapp/public/gameImg/foto7.jpg b/webapp/public/gameImg/foto7.jpg deleted file mode 100644 index e213a172..00000000 Binary files a/webapp/public/gameImg/foto7.jpg and /dev/null differ diff --git a/webapp/public/hurta2.jpg b/webapp/public/hurta2.jpg new file mode 100644 index 00000000..33a719f6 Binary files /dev/null and b/webapp/public/hurta2.jpg differ diff --git a/webapp/public/hurtado.jpg b/webapp/public/hurtado.jpg new file mode 100644 index 00000000..0534ae71 Binary files /dev/null and b/webapp/public/hurtado.jpg differ diff --git a/webapp/public/possibleFavicon.ico b/webapp/public/possibleFavicon.ico new file mode 100644 index 00000000..9426db0f Binary files /dev/null and b/webapp/public/possibleFavicon.ico differ diff --git a/webapp/public/possibleIcon.jpg b/webapp/public/possibleIcon.jpg new file mode 100644 index 00000000..4fc25895 Binary files /dev/null and b/webapp/public/possibleIcon.jpg differ diff --git a/webapp/public/sounds/success_sound.mp3 b/webapp/public/sounds/success_sound.mp3 new file mode 100644 index 00000000..ff101229 Binary files /dev/null and b/webapp/public/sounds/success_sound.mp3 differ diff --git a/webapp/public/sounds/wrong_sound.mp3 b/webapp/public/sounds/wrong_sound.mp3 new file mode 100644 index 00000000..33dc9d2d Binary files /dev/null and b/webapp/public/sounds/wrong_sound.mp3 differ diff --git a/webapp/src/App.js b/webapp/src/App.js index 064f3bcb..7442c3a1 100644 --- a/webapp/src/App.js +++ b/webapp/src/App.js @@ -7,6 +7,8 @@ import Footer from './components/Footer'; import Home from './pages/Home'; import Homepage from './pages/Homepage'; import Game from './pages/Game'; +import GroupList from './pages/GroupList'; +import GroupCreate from './pages/GroupCreate'; import {Route, Routes} from 'react-router-dom'; import { createTheme, ThemeProvider } from '@mui/material/styles'; import { Box } from '@mui/material'; @@ -27,6 +29,9 @@ const theme = createTheme({ }); function App() { + React.useEffect(() => { + document.title = "WIQ - Wikidata Infinite Quest"; + }, []); return ( @@ -38,7 +43,8 @@ function App() { }/> }/> }/> - + }/> + }/> diff --git a/webapp/src/SessionContext.js b/webapp/src/SessionContext.js new file mode 100644 index 00000000..b4cf96ad --- /dev/null +++ b/webapp/src/SessionContext.js @@ -0,0 +1,52 @@ +import React, { createContext, useState, useEffect } from 'react'; +import { v4 as uuidv4 } from 'uuid'; + +const SessionContext = createContext(); + +const SessionProvider = ({ children }) => { + + const [sessionId, setSessionId] = useState(''); + const [username, setUsername] = useState(''); + const [isLoggedIn, setIsLoggedIn] = useState(false); + + // This hook recovers user data if available in localstorage when the sessprovider is created + // useEffect(() => { + // const storedSessionId = localStorage.getItem('sessionId'); + // if (storedSessionId) { + // setSessionId(storedSessionId); + // setIsLoggedIn(true); + + // // Here you can get the username using the sessionID + // const storedUsername = localStorage.getItem('username'); + // if (storedUsername) { + // setUsername(storedUsername); + // } + // } + // }, []); + + const createSession = (username) => { + const newSessionId = uuidv4(); + setSessionId(newSessionId); + setUsername(username); + setIsLoggedIn(true); + localStorage.setItem('sessionId', newSessionId); + localStorage.setItem('username', username); + }; + + const destroySession = () => { + localStorage.removeItem('sessionId'); + localStorage.removeItem('username'); + setSessionId(''); + setIsLoggedIn(false); + setUsername(''); + }; + + return ( + // This values are the props we can access from the child objects + + {children} + + ); + }; + +export { SessionContext, SessionProvider }; \ No newline at end of file diff --git a/webapp/src/components/Footer.js b/webapp/src/components/Footer.js index 3632b7aa..139082f1 100644 --- a/webapp/src/components/Footer.js +++ b/webapp/src/components/Footer.js @@ -1,17 +1,15 @@ import * as React from 'react'; -import { BottomNavigation, Toolbar, Typography, useTheme } from '@mui/material'; +import { AppBar, Toolbar, Typography } from '@mui/material'; const Footer = () => { - const theme = useTheme(); - return ( - + © WIQ_ES04A - + ); }; diff --git a/webapp/src/components/NavBar.js b/webapp/src/components/NavBar.js index de728613..d588723d 100644 --- a/webapp/src/components/NavBar.js +++ b/webapp/src/components/NavBar.js @@ -1,7 +1,9 @@ import * as React from 'react'; +import { useContext } from 'react'; import { AppBar, Toolbar, Menu, MenuItem, Box, Button, IconButton, Typography, Avatar } from '@mui/material'; import MenuIcon from '@mui/icons-material/Menu'; import { Link } from 'react-router-dom'; +import { SessionContext } from '../SessionContext'; // List of site pages for the menu. We have to address if it wouldnt be more consistent to extract this to a fragment / global const as it could be used outside. // Also as the element added is subjected to internazionalization, so we ll have to address it @@ -10,12 +12,22 @@ const pages = [ { path: '/homepage', text: 'Play' }, { path: '/statistics', text: 'Statistics' }, { path: '/instructions', text: 'Instructions' }, + { path: '/group/list', text: 'List Groups' }, + { path: '/group/create', text: 'Create Group' }, // Add an object for each new page ]; +// Provisional settings menu for a logged user +const settings = [ + { path: '/profile', text: 'Perfil' }, + { path: '/', text: 'Cerrar Sesión' } +] + function NavBar() { // Width for the nav menu element (?) Is it used later as a boolean ?????? const [anchorElNav, setAnchorElNav] = React.useState(null); + const [anchorElUser, setAnchorElUser] = React.useState(null); + const { username, isLoggedIn, destroySession } = useContext(SessionContext); const handleOpenNavMenu = (event) => { setAnchorElNav(event.currentTarget); @@ -25,11 +37,22 @@ function NavBar() { setAnchorElNav(null); }; + const handleOpenUserMenu = (event) => { + setAnchorElUser(event.currentTarget); + }; + + const handleCloseUserMenu = () => { + setAnchorElUser(null); + }; + + const handleLogout = () => { + handleCloseUserMenu(); + destroySession(); + }; + return ( // position="static" => Barra se desplaza con scroll down - {/* The Container component is used to limit the maximum width of the content inside the AppBar. It ensures that the content doesn't extend too far horizontally. */} - {/* */} {/* disableGutters -> Remove toolbar's padding */} {/* Menú de Navegación, sólo se muestra en dispositivos móviles */} @@ -71,29 +94,73 @@ function NavBar() { ))} - + - {/* Pages list in NavBar, only displayed when menu button is not, i.e., in larger devices */} - - {pages.map((page) => ( - - {page.text} - - ))} - - {/* Pending: auth depending: if not auth: log in else: menu */} - - - Log In - - - - - + + {/* Pages list in NavBar, only displayed when menu button is not, i.e., in larger devices */} + {isLoggedIn ? ( + + {pages.map((page) => ( + + {page.text} + + ))} + + ):( + + )} + + + {isLoggedIn ? ( + + + + {username} + + + {/* Need to change the image for the user profile one */} + + + + + {settings.map((setting) => ( + + + + {setting.text} + + + ))} + + + ):( + + + Log In + + + + + + )} - {/* */} ); } diff --git a/webapp/src/data/gameInfo.json b/webapp/src/data/gameInfo.json index a80791b7..180e19b5 100644 --- a/webapp/src/data/gameInfo.json +++ b/webapp/src/data/gameInfo.json @@ -3,7 +3,7 @@ "nombre": "WISE MEN STACK", "descripcion": "The player chooses a topic from five available options and must answer a battery of questions related to it within 60 seconds. For each question, the host provides two options. If the contestant guesses correctly, they win €20; otherwise, they move on to the next question (as the correct answer would be the other option). If the time runs out before the question is fully asked and both possible answers are provided, the contestant may still answer it; however, if the statement hasn't been completed (or the options weren't provided), they cannot answer.", - "foto": "../gameImg/foto0.jpg" + "foto": "../gameImg/foto1.jpg" }, { "nombre": "DISCARDING", @@ -18,26 +18,26 @@ { "nombre": "DISCOVERING CITIES", "descripcion": "Through photographs and clues provided by Pilar Vázquez, the contestants must guess the city proposed by the program that day. The contestant who goes first after the 'Warm question' will have the right to answer first and for 300 points. If they guess it correctly, they earn the points; if not, there will be a rebound, an additional clue, and a reduction of 100 points for each contestant.", - "foto": "../gameImg/foto3.jpg" + "foto": "../gameImg/foto1.jpg" }, { "nombre": "LAST CALL", "descripcion": "The three contestants are positioned according to the scores obtained so far in the program. Six questions related to a specific topic are asked. Before starting, the answers to the six questions are provided. If the contestant's answer is incorrect, €100 is deducted from their score, and it contributes to a jackpot (which starts at zero). If the contestant answers correctly, they add the amount accumulated in the jackpot to their score, and the jackpot resets to zero (if there is nothing accumulated, the score remains the same). The first question is directed to the participant with the highest score, the next to the second highest, and the third to the lowest scorer. For the next three questions, this order is repeated.", - "foto": "../gameImg/foto4.jpg" + "foto": "../gameImg/foto1.jpg" }, { "nombre": "THE HUMAN CALCULATOR", "descripcion": "Introduced in 2003, this challenge is the most feared after the main game. In it, the contestant who placed second in Last Call (previously, the loser in The Duel) faces the resolution of 7 arithmetic operations in 30 seconds. In 2021, it was slightly modified to include operations with indirect numbers (e.g., years in a lustrum, dozens, elements that make up something...). If they solve them, they keep the points earned for the day, but if not, they lose all accumulated points for the day (since the introduction of the mixed term, they lose 50% of the day's accumulation).", - "foto": "../gameImg/foto5.jpg" + "foto": "../gameImg/foto3.jpg" }, { "nombre": "THE PART FOR THE WHOLE", "descripcion": "This challenge allows one of the contestants to win an extra sum of money, directly added to their total accumulated. The contestants must discover a 'whole', related to each of the 'parts' provided by Elisenda as clues. For their answer to be considered valid, besides discovering the whole, they must describe correctly how each of the parts relates to that whole. All contestants can participate, but in a specific order depending on the score obtained during the program. The game can last for several days until a contestant has the correct and complete answer. First day, the prize starts at 1000 euros, and decreases by 100 euros for each day that passes.", - "foto": "../gameImg/foto6.jpg" + "foto": "../gameImg/foto1.jpg" }, { "nombre": "THE CHALLENGE", "descripcion": "In the early days of the program, there was no challenge, so after the hot question, there was always an elimination. This challenge was incorporated in 1998. It is one of the audience's favorite challenges, despite being an elimination round. The contestant with the lowest score after the last call (previously, the contestant with the lowest score after the hot question) gains access to this challenge. The contestant is presented with seven words, of which they only know the first three letters, and with Elisenda Roca's definitions, they must guess all the words in less than 50 seconds. If they guess them correctly, they will participate in the next program; otherwise, their participation ends. In both cases, the points earned that day are added to the total.", - "foto": "../gameImg/foto7.jpg" + "foto": "../gameImg/foto4.jpg" } ] \ No newline at end of file diff --git a/webapp/src/index.css b/webapp/src/index.css index ec2585e8..5bcaa367 100644 --- a/webapp/src/index.css +++ b/webapp/src/index.css @@ -1,10 +1,8 @@ body { + background-image: url('../public/hurta2.jpg'); + background-size: cover; + background-repeat: no-repeat; margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; } code { diff --git a/webapp/src/index.js b/webapp/src/index.js index 03976ff6..afcccc27 100644 --- a/webapp/src/index.js +++ b/webapp/src/index.js @@ -4,12 +4,14 @@ import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; import {BrowserRouter} from 'react-router-dom'; - +import { SessionProvider } from './SessionContext'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( - + + + ); diff --git a/webapp/src/pages/AddUser.js b/webapp/src/pages/AddUser.js index 40231592..c8288439 100644 --- a/webapp/src/pages/AddUser.js +++ b/webapp/src/pages/AddUser.js @@ -1,4 +1,3 @@ -// src/components/AddUser.js import React, { useState } from 'react'; import axios from 'axios'; import { Container, Typography, TextField, Button, Snackbar, Box, Divider } from '@mui/material'; diff --git a/webapp/src/pages/Game.js b/webapp/src/pages/Game.js index ccec46ed..bc1f053a 100644 --- a/webapp/src/pages/Game.js +++ b/webapp/src/pages/Game.js @@ -5,14 +5,20 @@ import { Container, Button, CssBaseline, Grid, Typography, CircularProgress } fr import CheckIcon from '@mui/icons-material/Check'; import ClearIcon from '@mui/icons-material/Clear'; import { useNavigate } from 'react-router-dom'; +import { SessionContext } from '../SessionContext'; +import { useContext } from 'react'; -const MAX_ROUNDS = 3; //const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8010'; const apiEndpoint = 'http://localhost:8000'; - const Game = () => { const navigate = useNavigate(); + const MAX_ROUNDS = 3; + const SUCCESS_SOUND_ROUTE = "/sounds/success_sound.mp3"; + const FAILURE_SOUND_ROUTE = "/sounds/wrong_sound.mp3"; + + //sesion information + const {username} = useContext(SessionContext); // state initialization const [round, setRound] = React.useState(1); @@ -20,28 +26,22 @@ const Game = () => { const [buttonStates, setButtonStates] = React.useState([]); const [answered, setAnswered] = React.useState(false); const [shouldRedirect, setShouldRedirect] = React.useState(false); - const [userData, setUserData] = React.useState({ - username: "Samu11", //change it - total_score: 0, - correctly_answered_questions: 0, - incorrectly_answered_questions: 0, - total_time_played: 0, - games_played: 1, - }); + const [totalScore, setTotalScore] = React.useState(0); + const [correctlyAnsweredQuestions, setCorrectlyAnsweredQuestions] = React.useState(0); + const [incorrectlyAnsweredQuestions, setIncorrectlyAnsweredQuestions] = React.useState(0); + const [totalTimePlayed, setTotalTimePlayed] = React.useState(0); + const [gamesPlayed, setGamesPlayed] = React.useState(1); const [timerRunning, setTimerRunning] = React.useState(true); // indicate if the timer is working - // hook to iniciate timer React.useEffect(() => { let timer; if (timerRunning) { timer = setInterval(() => { - setUserData((prevUserData) => ({ - ...prevUserData, - total_time_played: prevUserData.total_time_played + 1 - })); - }, 1000); + setTotalTimePlayed((prevTotalTime) => prevTotalTime + 1); + }, 1000); } - return () => clearInterval(timer); + + return () => clearInterval(timer); }, [timerRunning]); // hook to initiating new rounds if the current number of rounds is less than or equal to 3 @@ -76,22 +76,22 @@ const Game = () => { //check answer if (response === questionData.correctAnswer) { newButtonStates[index] = "success" - setUserData((prevUserData) => ({ - ...prevUserData, - correctly_answered_questions: prevUserData.correctly_answered_questions + 1, - total_score: prevUserData.total_score + 20, - })); + const sucessSound = new Audio(SUCCESS_SOUND_ROUTE); + sucessSound.volume = 0.40; + sucessSound.play(); + setCorrectlyAnsweredQuestions(correctlyAnsweredQuestions + 1); + setTotalScore(totalScore + 20); } else { newButtonStates[index] = "failure"; + const failureSound = new Audio(FAILURE_SOUND_ROUTE); + failureSound.volume = 0.40; + failureSound.play(); for (let i = 0; i < questionData.options.length; i++) { if (questionData.options[i] === questionData.correctAnswer) { newButtonStates[i] = "success"; } } - setUserData(prevUserData => ({ - ...prevUserData, - incorrectly_answered_questions: prevUserData.incorrectly_answered_questions + 1, - })); + setIncorrectlyAnsweredQuestions(incorrectlyAnsweredQuestions + 1); } setButtonStates(newButtonStates); @@ -99,22 +99,17 @@ const Game = () => { if (round >= 3) { // Update user data before redirecting try { - const response = await fetch('/user/edit', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(userData), - }); - - if (response.ok) { - console.log('User data updated successfully'); - } else { - console.error('Failed to update user data:', response.statusText); - } - } catch (error) { - console.error('Error updating user data:', error); - } + await axios.post(`${apiEndpoint}/user/edit`, { + username:username, + total_score:totalScore, + correctly_answered_questions:correctlyAnsweredQuestions, + incorrectly_answered_questions:incorrectlyAnsweredQuestions, + total_time_played:totalTimePlayed, + games_played:gamesPlayed + }); + } catch (error) { + console.error("Error:", error); + } } setTimeout(() => { @@ -165,19 +160,19 @@ if (shouldRedirect) { userData.incorrectly_answered_questions ? 'green' : 'red', + color: correctlyAnsweredQuestions > incorrectlyAnsweredQuestions ? 'green' : 'red', fontSize: '4rem', // Tamaño de fuente marginTop: '20px', // Espaciado superior marginBottom: '50px', // Espaciado inferior }} > - {userData.correctly_answered_questions > userData.incorrectly_answered_questions ? "Great Job!" : "Game Over"} + {correctlyAnsweredQuestions > incorrectlyAnsweredQuestions ? "Great Job!" : "Game Over"} - Correct Answers: {userData.correctly_answered_questions} - Incorrect Answers: {userData.incorrectly_answered_questions} - Total money: {userData.total_score} - Time: {userData.total_time_played} seconds + Correct Answers: {correctlyAnsweredQuestions} + Incorrect Answers: {incorrectlyAnsweredQuestions} + Total money: {totalScore} + Time: {totalTimePlayed} seconds ); @@ -202,7 +197,7 @@ if (shouldRedirect) { right: '5%', }} > - Time: {userData.total_time_played} s + Time: {totalTimePlayed} s {round} / {MAX_ROUNDS} diff --git a/webapp/src/pages/GroupCreate.js b/webapp/src/pages/GroupCreate.js new file mode 100644 index 00000000..24ebebe1 --- /dev/null +++ b/webapp/src/pages/GroupCreate.js @@ -0,0 +1,58 @@ +import React, { useState,useContext } from 'react'; +import axios from 'axios'; +import { Container, Typography, TextField, Button, Snackbar, Box, Divider } from '@mui/material'; +import { SessionContext } from '../SessionContext'; + +const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000'; + +const AddGroup = () => { + const [name, setName] = useState(''); + const [error, setError] = useState(''); + const [openSnackbar, setOpenSnackbar] = useState(false); + + const { username } = useContext(SessionContext); + + const addGroup = async () => { + try { + await axios.post(`${apiEndpoint}/group/add`, { + name:name, + username:username + }); + setOpenSnackbar(true); + } catch (error) { + setError(error.response.data.error); + } + }; + + const handleCloseSnackbar = () => { + setOpenSnackbar(false); + }; + + return ( + + + + + Create a Group + + setName(e.target.value)} + /> + + + Create + + + {error && ( setError('')} message={`Error: ${error}`} />)} + + + + ); +}; + +export default AddGroup; \ No newline at end of file diff --git a/webapp/src/pages/GroupList.js b/webapp/src/pages/GroupList.js new file mode 100644 index 00000000..84e0c367 --- /dev/null +++ b/webapp/src/pages/GroupList.js @@ -0,0 +1,45 @@ +import React, { useEffect, useState } from 'react'; +import axios from 'axios'; +import { Container, Typography, List, ListItem, ListItemText, Button, Divider, Box } from '@mui/material'; + +const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000'; + +const GroupList = () => { + const [groups, setGroups] = useState([]); + useEffect(() => { + const fetchData = async () => { + try { + const response = await axios.get(`${apiEndpoint}/group/list`); + console.log(response.data.groups); + setGroups(response.data.groups); + } catch (error) { + console.error('Error fetching data:', error); + } + }; + + fetchData(); + }, []); + + return ( + + GROUPS + + + {groups.map((group) => ( + + + + + Unirse + + + + + ))} + + + ); + +} + +export default GroupList; \ No newline at end of file diff --git a/webapp/src/pages/Home.js b/webapp/src/pages/Home.js index cd486e0a..f7197321 100644 --- a/webapp/src/pages/Home.js +++ b/webapp/src/pages/Home.js @@ -1,29 +1,20 @@ -import * as React from 'react'; -import ImageSlider from '../components/ImageSlider'; -import { Box, Typography, Paper, useTheme } from '@mui/material'; -import data from "../data/sliderData.json"; +import * as React from "react"; +import { Box, Typography, useTheme } from "@mui/material"; const Home = () => { const theme = useTheme(); return ( - - - - - WIKIDATA - INFINITE - QUEST - - - Welcome to WIQ, also known as Wikidata Infinite Quest. On this page, a vast array of challenges awaits you, which will help you become the most knowledgeable person in the world. - To achieve this, you'll need to complete various mini-games. But don't worry, thanks to our dynamic question system, you can play as many times as you want since the questions are infinite. - From here, HappySoftware in collaboration with RTVE wishes you luck and hopes to see you at the top of the leaderboard. - + + + + WIKIDATA + INFINITE + QUEST - - - + + WELCOME TO WIQ, WHERE INFINITE KNOWLEDGE AWAITS POWERED BY WIKIDATA.LOG IN AND START PLAYING MINI-GAMES BASED ON 'SABER Y GANAR' TO BECOME THE ULTIMATE CHAMPION. HAPPYSOFTWARE WISHES YOU LUCK ON YOUR QUEST TO THE TOP! + ); }; diff --git a/webapp/src/pages/Instructions.js b/webapp/src/pages/Instructions.js index ef12af42..beadf430 100644 --- a/webapp/src/pages/Instructions.js +++ b/webapp/src/pages/Instructions.js @@ -1,30 +1,44 @@ -import * as React from 'react'; -import { Container } from '@mui/material'; -import CssBaseline from '@mui/material/CssBaseline'; -import { Button, Typography, Box, Grid } from '@mui/material'; -import data from '../data/gameInfo.json'; +import * as React from "react"; +import { Container } from "@mui/material"; +import CssBaseline from "@mui/material/CssBaseline"; +import { Button, Typography, Grid } from "@mui/material"; +import data from "../data/gameInfo.json"; const Instructions = () => { + // Whole information about games const [info, setInfo] = React.useState(null); + + // Game to show info about and the comp with the info + const [gameDisplayedIndex, setGameDisplayed] = React.useState(null); const [gameInfo, setGameInfo] = React.useState(null); React.useEffect(() => { setInfo(data); }, []); - const newGameInfo = (index) => { - setGameInfo( - - - - - - {info[index].nombre} - {info[index].descripcion} - - - ); + const displayGameInfo = (index) => { + // If game being displayed is selected, hides the info panel + if (gameDisplayedIndex === index) { + setGameInfo(null); + setGameDisplayed(null); + console.log(gameDisplayedIndex); + } + else { + setGameInfo( + + + + + + + {info[index].nombre} + {info[index].descripcion} + + + ); + setGameDisplayed(index); + } }; if (!info) { @@ -32,15 +46,15 @@ const Instructions = () => { } return ( - + - - - GAMEPLAY & MODES: + + + GAME MODES {info.map((option, index) => ( - - newGameInfo([index])}> + + displayGameInfo(index)} > {option.nombre} diff --git a/webapp/src/pages/Login.js b/webapp/src/pages/Login.js index 5dc78fc5..187bfd67 100644 --- a/webapp/src/pages/Login.js +++ b/webapp/src/pages/Login.js @@ -1,8 +1,9 @@ // src/components/Login.js -import React, { useState } from 'react'; +import React, { useState,useContext } from 'react'; import axios from 'axios'; import { Container, Typography, TextField, Button, Snackbar, Box, Divider } from '@mui/material'; import { Link } from 'react-router-dom'; +import { SessionContext } from '../SessionContext'; const Login = () => { const [username, setUsername] = useState(''); @@ -12,19 +13,22 @@ const Login = () => { const [createdAt, setCreatedAt] = useState(''); const [openSnackbar, setOpenSnackbar] = useState(false); + const { createSession } = useContext(SessionContext); + const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000'; const loginUser = async () => { try { + const response = await axios.post(`${apiEndpoint}/login`, { username, password }); // Extract data from the response const { createdAt: userCreatedAt } = response.data; - setCreatedAt(userCreatedAt); setLoginSuccess(true); - setOpenSnackbar(true); + createSession(username); + } catch (error) { setError(error.response.data.error); }