diff --git a/webapp/package-lock.json b/webapp/package-lock.json index ab9e20c..fb67267 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -11,15 +11,18 @@ "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", "@mui/material": "^5.15.3", + "@react-oauth/google": "^0.12.1", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^14.1.2", "@testing-library/user-event": "^14.5.2", "axios": "^1.6.5", "gapi-script": "^1.2.0", "i18next": "^23.10.0", + "jwt-decode": "^4.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-i18next": "^14.0.5", + "react-router-dom": "^6.22.2", "react-scripts": "^5.0.1", "web-vitals": "^3.5.1" }, @@ -4969,6 +4972,23 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "dev": true }, + "node_modules/@react-oauth/google": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.12.1.tgz", + "integrity": "sha512-qagsy22t+7UdkYAiT5ZhfM4StXi9PPNvw0zuwNmabrWyMKddczMtBIOARflbaIj+wHiQjnMAsZmzsUYuXeyoSg==", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remix-run/router": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.2.tgz", + "integrity": "sha512-+Rnav+CaoTE5QJc4Jcwh5toUpnVLKYbpU6Ys0zqbakqbaLQHeglLVHPfxOiQqdNmUy5C2lXz5dwC6tQNX2JW2Q==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -18193,6 +18213,14 @@ "node": ">=4.0" } }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "engines": { + "node": ">=18" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -21564,6 +21592,36 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.22.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.2.tgz", + "integrity": "sha512-YD3Dzprzpcq+tBMHBS822tCjnWD3iIZbTeSXMY9LPSG541EfoBGyZ3bS25KEnaZjLcmQpw2AVLkFyfgXY8uvcw==", + "dependencies": { + "@remix-run/router": "1.15.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.22.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.2.tgz", + "integrity": "sha512-WgqxD2qySEIBPZ3w0sHH+PUAiamDeszls9tzqMPBDA1YYVucTBXLU7+gtRfcSnhe92A3glPnvSxK2dhNoAVOIQ==", + "dependencies": { + "@remix-run/router": "1.15.2", + "react-router": "6.22.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", diff --git a/webapp/package.json b/webapp/package.json index 000714e..37bae60 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -6,15 +6,18 @@ "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", "@mui/material": "^5.15.3", + "@react-oauth/google": "^0.12.1", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^14.1.2", "@testing-library/user-event": "^14.5.2", "axios": "^1.6.5", "gapi-script": "^1.2.0", "i18next": "^23.10.0", + "jwt-decode": "^4.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-i18next": "^14.0.5", + "react-router-dom": "^6.22.2", "react-scripts": "^5.0.1", "web-vitals": "^3.5.1" }, diff --git a/webapp/src/App.css b/webapp/src/App.css index d9eb681..74b5e05 100644 --- a/webapp/src/App.css +++ b/webapp/src/App.css @@ -28,18 +28,6 @@ color: #61dafb; } -.app-button{ - color: primary; - cursor: pointer; - margin-left: 16px; - border: 0; - border-radius: 999px; - padding: 6px 16px; - font-weight: bold; - border: 1px solid black; - transition: .3s ease background-color; -} - @keyframes App-logo-spin { from { transform: rotate(0deg); diff --git a/webapp/src/App.test.js b/webapp/src/App.test.js deleted file mode 100644 index 419d874..0000000 --- a/webapp/src/App.test.js +++ /dev/null @@ -1,8 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import App from './App'; - -test('renders learn react link', () => { - render(); - const linkElement = screen.getByText(/Conocer y Vencer/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx index 6b6eecf..ccafa9e 100644 --- a/webapp/src/App.tsx +++ b/webapp/src/App.tsx @@ -1,51 +1,12 @@ -import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import AddUser from './components/AddUser'; -import Login from './components/Login'; -import CssBaseline from '@mui/material/CssBaseline'; -import Container from '@mui/material/Container'; -import Typography from '@mui/material/Typography'; -import Init from './components/Init'; -import './i18n'; -/* import GoogleLoginMenu from './components/GoogleLoginMenu'; */ +import { BrowserRouter } from "react-router-dom"; +import { AppRouter } from "./Router"; +/** The old code is not in /pages/init/index.tsx and is shown as default */ function App() { - const { t } = useTranslation() - // const [showGoogleLM, setShowGoogleLM] = useState(false); - const [showLogin, setShowLogin] = useState(true); - const [showInit, setShowInit] = useState(true); - - const handleToggleView = (state: boolean) => { - setShowLogin(state); - }; - - const handleLoginRegisterToggleView = (state?:boolean) => { - setShowInit(!showInit); - state? handleToggleView(state) : handleToggleView(false) - }; - - /* const handleGoogleViewChange = () => { - setShowGoogleLM(!showGoogleLM); - setShowInit(!showInit); - } */ - return ( - - - - {t('app_name')} - - - {showInit ? - - /* : showGoogleLM ? - */ - : showLogin ? - - : } - - - + + + ); } diff --git a/webapp/src/Router.tsx b/webapp/src/Router.tsx new file mode 100644 index 0000000..b4fd61a --- /dev/null +++ b/webapp/src/Router.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import { Route, Routes } from "react-router-dom"; +import { GamePage } from "./pages/game"; +import { GroupsPage } from "./pages/groups"; +import { RouterLayout } from "./common/RouterLayout"; +import { InitPage } from "./pages/init"; +import { ScoreboardPage } from "./pages/scoreboard"; + +export const AppRouter: React.FC<{}> = () => { + return ( + + }> + /* when accessing /game or the other paths, it will be shown as the + outlet inside the RouterLayout*/ + } /> + } /> + } /> + + } /> + + ); +}; \ No newline at end of file diff --git a/webapp/src/common/Nav.tsx b/webapp/src/common/Nav.tsx new file mode 100644 index 0000000..ca76b40 --- /dev/null +++ b/webapp/src/common/Nav.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import './nav.css'; +import { useTranslation } from 'react-i18next'; +import {AppBar, Container, Toolbar, Grid, Stack, Button} from "@mui/material"; +import { useNavigate } from "react-router-dom"; + +const NavBar: React.FC<{}> = () => { + const { t } = useTranslation(); + const navigate = useNavigate(); + return ( + + + + + + {t('app_name')} + + + + + + + + + + + + + ) +}; + +export default NavBar; \ No newline at end of file diff --git a/webapp/src/common/RouterLayout.tsx b/webapp/src/common/RouterLayout.tsx new file mode 100644 index 0000000..b828ad5 --- /dev/null +++ b/webapp/src/common/RouterLayout.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import { Outlet } from "react-router-dom"; +import NavBar from "./Nav"; + +/* This shows the NavBar and behind the element passed here */ +export const RouterLayout: React.FC<{}> = () => { + return( + <> + + + + ) +}; \ No newline at end of file diff --git a/webapp/src/common/nav.css b/webapp/src/common/nav.css new file mode 100644 index 0000000..4dc8938 --- /dev/null +++ b/webapp/src/common/nav.css @@ -0,0 +1,30 @@ +.navbar { + width:100%; + height: 50px; + display: flex; + align-items: center; + +} + +.nav_appBar{ + background: #D9D9D9; + position: fixed; + justify-content: space-between; +} + +.logo{ + font-weight: 500; + font-size: large; +} + +.link-container{ + margin-right: 20px; + display:flex; + width: 300px; + justify-content: space-evenly; + cursor: pointer; +} + +.link{ + text-decoration: none; +} \ No newline at end of file diff --git a/webapp/src/components/GLoginButton.tsx b/webapp/src/components/GLoginButton.tsx index 3242919..4f3ee49 100644 --- a/webapp/src/components/GLoginButton.tsx +++ b/webapp/src/components/GLoginButton.tsx @@ -2,7 +2,6 @@ import { useTranslation } from 'react-i18next'; -const GClientId = "http://259836370797-brpmuu6pn6a20eecpjag1l2nkoqp3eo6.apps.googleusercontent.com"; const GLoginButton = () => { const { t } = useTranslation(); @@ -47,4 +46,21 @@ const GLoginButton = () => { ) } -export default GLoginButton; */ \ No newline at end of file +export default GLoginButton; */ + +import { GoogleLogin } from '@react-oauth/google'; + +const GLoginButton = () => { + + return( + { + console.log(credentialResponse); + }} + onError={() => { + console.log('Login Failed'); + }} + />) +} + +export default GLoginButton; \ No newline at end of file diff --git a/webapp/src/components/Game.tsx b/webapp/src/components/Game.tsx new file mode 100644 index 0000000..66545a6 --- /dev/null +++ b/webapp/src/components/Game.tsx @@ -0,0 +1,11 @@ + + +const Game = () => { + return ( +
+

Game

+
+ ) +} + +export default Game; // Export the 'Game' component \ No newline at end of file diff --git a/webapp/src/components/GameLayout.tsx b/webapp/src/components/GameLayout.tsx new file mode 100644 index 0000000..132166b --- /dev/null +++ b/webapp/src/components/GameLayout.tsx @@ -0,0 +1,49 @@ + + +import { useState } from "react"; +import Game from "./Game"; +import Group from "./Group"; +import Scoreboard from "./Scoreboard"; + + +const GameLayout = () => { + +const [currentView, setCurrentView] = useState("Game"); + + + +return( + + + + + + import Game from "./Game"; // Import the 'Game' component + + + {currentView === "Game" ? : + currentView === "Group" ? : + } + + + +) + + + +}; export default GameLayout; // Export the 'GameLayout' component \ No newline at end of file diff --git a/webapp/src/components/GoogleLoginMenu.tsx b/webapp/src/components/GoogleLoginMenu.tsx deleted file mode 100644 index 145fab8..0000000 --- a/webapp/src/components/GoogleLoginMenu.tsx +++ /dev/null @@ -1,38 +0,0 @@ - -/* import { Container} from '@mui/material'; -import { useTranslation } from 'react-i18next'; -import GLoginButton from './GLoginButton'; - - -type ActionProps = { - goBack:()=> void; -} - -const GoogleLogin = (props: ActionProps) => { - - const { t } = useTranslation(); - - - return( - - - -

{t("login_google")}

- - -
- - -
- - -
- - ) - - -} - -export default GoogleLogin; */ \ No newline at end of file diff --git a/webapp/src/components/Group.tsx b/webapp/src/components/Group.tsx new file mode 100644 index 0000000..1c0b8e1 --- /dev/null +++ b/webapp/src/components/Group.tsx @@ -0,0 +1,12 @@ + + +const Group=()=>{ + + return( +
+

Group

+
+ ) +} + +export default Group; \ No newline at end of file diff --git a/webapp/src/components/Init.tsx b/webapp/src/components/Init.tsx index e5f918c..00a03c7 100644 --- a/webapp/src/components/Init.tsx +++ b/webapp/src/components/Init.tsx @@ -1,4 +1,6 @@ import { useTranslation } from 'react-i18next'; +import {Button, Stack} from "@mui/material"; +import GLoginButton from './GLoginButton'; type ActionProps = { changeView:(arg:boolean)=> void; @@ -8,20 +10,15 @@ type ActionProps = { const Init = (props:ActionProps) =>{ const { t } = useTranslation() return ( -
- - + - {/* */} -
+ + + ); }; diff --git a/webapp/src/components/Login.test.js b/webapp/src/components/Login.test.js deleted file mode 100644 index fb4258b..0000000 --- a/webapp/src/components/Login.test.js +++ /dev/null @@ -1,63 +0,0 @@ -import React from 'react'; -import { render, fireEvent, screen, waitFor, act } from '@testing-library/react'; -import axios from 'axios'; -import MockAdapter from 'axios-mock-adapter'; -import Login from './Login'; - -const mockAxios = new MockAdapter(axios); - -describe('Login component', () => { - - beforeEach(() => { - mockAxios.reset(); - }); - - it('should log in successfully', async () => { - render(); - - const usernameInput = screen.getByLabelText(/Username/i); - const passwordInput = screen.getByLabelText(/Password/i); - const loginButton = screen.getByRole('button', { name: /Login/i }); - - // Mock the axios.post request to simulate a successful response - mockAxios.onPost('http://localhost:8000/login').reply(200, { createdAt: '2024-01-01T12:34:56Z' }); - - // Simulate user input - await act(async () => { - fireEvent.change(usernameInput, { target: { value: 'testUser' } }); - fireEvent.change(passwordInput, { target: { value: 'testPassword' } }); - fireEvent.click(loginButton); - }); - - // Verify that the user information is displayed - expect(screen.getByText(/Hello testUser!/i)).toBeInTheDocument(); - expect(screen.getByText(/Your account was created on 1\/1\/2024/i)).toBeInTheDocument(); - }); - - it('should handle error when logging in', async () => { - render(); - - const usernameInput = screen.getByLabelText(/Username/i); - const passwordInput = screen.getByLabelText(/Password/i); - const loginButton = screen.getByRole('button', { name: /Login/i }); - - // Mock the axios.post request to simulate an error response - mockAxios.onPost('http://localhost:8000/login').reply(401, { error: 'Unauthorized' }); - - // Simulate user input - fireEvent.change(usernameInput, { target: { value: 'testUser' } }); - fireEvent.change(passwordInput, { target: { value: 'testPassword' } }); - - // Trigger the login button click - fireEvent.click(loginButton); - - // Wait for the error Snackbar to be open - await waitFor(() => { - expect(screen.getByText(/Error: Unauthorized/i)).toBeInTheDocument(); - }); - - // Verify that the user information is not displayed - expect(screen.queryByText(/Hello testUser!/i)).toBeNull(); - expect(screen.queryByText(/Your account was created on/i)).toBeNull(); - }); -}); diff --git a/webapp/src/components/Login.tsx b/webapp/src/components/Login.tsx index 9964a9f..81b6081 100644 --- a/webapp/src/components/Login.tsx +++ b/webapp/src/components/Login.tsx @@ -1,32 +1,32 @@ // src/components/Login.js import { useState } from 'react'; import axios from 'axios'; -import { Container, Typography, TextField, Snackbar } from '@mui/material'; +import { Container, Typography, TextField, Snackbar, Button, Stack } from '@mui/material'; import { useTranslation } from 'react-i18next'; +import { useNavigate } from "react-router-dom"; type ActionProps = { goBack:()=> void; } const Login = (props: ActionProps) => { + const navigate = useNavigate(); const { t } = useTranslation(); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(''); const [loginSuccess, setLoginSuccess] = useState(false); - const [createdAt, setCreatedAt] = useState(''); + const [openSnackbar, setOpenSnackbar] = useState(false); const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000'; const loginUser = async () => { try { - const response = await axios.post(`${apiEndpoint}/login`, { username, password }); + await axios.post(`${apiEndpoint}/login`, { username, password }); // Extract data from the response - const { createdAt: userCreatedAt } = response.data; - - setCreatedAt(userCreatedAt); + setLoginSuccess(true); setOpenSnackbar(true); @@ -37,21 +37,12 @@ const Login = (props: ActionProps) => { const handleCloseSnackbar = () => { setOpenSnackbar(false); + if(loginSuccess) + navigate("/game"); }; return ( - {loginSuccess ? ( -
- {/* poner aqui la aplicacion */} - - Hello {username}! - - - Your account was created on {new Date(createdAt).toLocaleDateString()}. - -
- ) : (
{t('login')} @@ -71,18 +62,19 @@ const Login = (props: ActionProps) => { value={password} onChange={(e) => setPassword(e.target.value)} /> - - + + + + {error && ( setError('')} message={`Error: ${error}`} /> )}
- )}
); }; diff --git a/webapp/src/components/Scoreboard.tsx b/webapp/src/components/Scoreboard.tsx new file mode 100644 index 0000000..26eccd1 --- /dev/null +++ b/webapp/src/components/Scoreboard.tsx @@ -0,0 +1,16 @@ + + + +const Scoreboard = ( + +) => { + + return ( +
+

Scoreboard

+

Here is the scoreboard

+
+ ) +} + +export default Scoreboard; \ No newline at end of file diff --git a/webapp/src/i18n.ts b/webapp/src/i18n.ts index 827b6c5..a246b67 100644 --- a/webapp/src/i18n.ts +++ b/webapp/src/i18n.ts @@ -17,7 +17,10 @@ i18n go_back: 'Go back', register: 'Register', add_user: 'Add user', - login_google: 'Login with Google' + login_google: 'Login with Google', + nav_game: 'Game', + nav_groups: 'Groups', + nav_scoreboard: 'Scoreboard', } }, es: { @@ -27,7 +30,10 @@ i18n go_back: 'Ir atrás', register: 'Registrarse', add_user: 'Añadir usuario', - login_google: 'Iniciar sesión con Google' + login_google: 'Iniciar sesión con Google', + nav_game: 'Juego', + nav_groups: 'Grupos', + nav_scoreboard: 'Tabla de puntos', } }, diff --git a/webapp/src/index.tsx b/webapp/src/index.tsx index 5b87300..7490de1 100644 --- a/webapp/src/index.tsx +++ b/webapp/src/index.tsx @@ -3,12 +3,16 @@ import ReactDOM from 'react-dom/client'; import './index.css'; import App from './App' import reportWebVitals from './reportWebVitals'; +import { GoogleOAuthProvider } from "@react-oauth/google" + const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); root.render( - - - + + + + + ); // If you want to start measuring performance in your app, pass a function diff --git a/webapp/src/pages/game/index.tsx b/webapp/src/pages/game/index.tsx new file mode 100644 index 0000000..d4f1ea4 --- /dev/null +++ b/webapp/src/pages/game/index.tsx @@ -0,0 +1,8 @@ +import React from "react"; +import Game from "../../components/Game"; + +export const GamePage: React.FC<{}> = () => { + return ( + + ); +}; \ No newline at end of file diff --git a/webapp/src/pages/groups/index.tsx b/webapp/src/pages/groups/index.tsx new file mode 100644 index 0000000..7e944e0 --- /dev/null +++ b/webapp/src/pages/groups/index.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import { Button, Container } from "@mui/material"; + +export const GroupsPage: React.FC<{}> = () => { + return ( + + + + ); +}; \ No newline at end of file diff --git a/webapp/src/pages/init/index.tsx b/webapp/src/pages/init/index.tsx new file mode 100644 index 0000000..9681d9a --- /dev/null +++ b/webapp/src/pages/init/index.tsx @@ -0,0 +1,49 @@ +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import AddUser from '../../components/AddUser'; +import Login from '../../components/Login'; +import CssBaseline from '@mui/material/CssBaseline'; +import Container from '@mui/material/Container'; +import Typography from '@mui/material/Typography'; +import Init from '../../components/Init'; +import '../../i18n'; + +/** Code that was beforehand in App.tsx */ +export const InitPage: React.FC<{}> = () =>{ + const { t } = useTranslation() + // const [showGoogleLM, setShowGoogleLM] = useState(false); + const [showLogin, setShowLogin] = useState(true); + const [showInit, setShowInit] = useState(true); + + const handleToggleView = (state: boolean) => { + setShowLogin(state); + }; + + const handleLoginRegisterToggleView = (state?:boolean) => { + setShowInit(!showInit); + state? handleToggleView(state) : handleToggleView(false) + }; + + /* const handleGoogleViewChange = () => { + setShowGoogleLM(!showGoogleLM); + setShowInit(!showInit); + } */ + + return ( + + + + {t('app_name')} + + + {showInit ? + + /* : showGoogleLM ? + */ + : showLogin ? + + : } + + /* changed the login button to /components/Init.tsx (where the other buttons are)*/ + ); +} \ No newline at end of file diff --git a/webapp/src/pages/scoreboard/index.tsx b/webapp/src/pages/scoreboard/index.tsx new file mode 100644 index 0000000..89f8b58 --- /dev/null +++ b/webapp/src/pages/scoreboard/index.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import { Button, Container } from "@mui/material"; + +export const ScoreboardPage: React.FC<{}> = () => { + return ( + + + + ); +}; \ No newline at end of file diff --git a/webapp/tsconfig.json b/webapp/tsconfig.json index b6cef81..0bceb75 100644 --- a/webapp/tsconfig.json +++ b/webapp/tsconfig.json @@ -9,6 +9,7 @@ "types":["node"], "jsx":"react-jsx", "module": "esnext", + "resolveJsonModule": true, "outDir": "lib", "removeComments": true,