add: merged front with back
This commit is contained in:
parent
1832ed12ec
commit
48f50a1daa
42 changed files with 5458 additions and 20 deletions
|
@ -1,2 +0,0 @@
|
|||
/node_modules
|
||||
package-lock.json
|
13
Dockerfile
13
Dockerfile
|
@ -1,5 +1,12 @@
|
|||
FROM node:alpine AS build
|
||||
WORKDIR /app
|
||||
COPY front .
|
||||
RUN npm install && npm run build
|
||||
|
||||
FROM node:alpine
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN npm i
|
||||
CMD ["npm", "run", "start"]
|
||||
COPY back .
|
||||
RUN npm install
|
||||
COPY --from=build /app/dist /app/public
|
||||
EXPOSE 3000
|
||||
CMD ["node", "index.js"]
|
0
.gitignore → back/.gitignore
vendored
0
.gitignore → back/.gitignore
vendored
|
@ -9,6 +9,10 @@ const router = express.Router();
|
|||
router.post('/', async (req, res) => {
|
||||
const {username, password} = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(400).send({error: "missing parameters"});
|
||||
}
|
||||
|
||||
const connection = await getConnection();
|
||||
const user = await getUser(connection, username);
|
||||
|
|
@ -9,11 +9,7 @@ const app = express();
|
|||
|
||||
app.use(express.json());
|
||||
app.use(cookieParser());
|
||||
app.use(cors({
|
||||
origin: ["https://joclud.leizour.fr", "http://localhost:5173"],
|
||||
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||
allowedHeaders: ["Content-Type", "Authorization"]
|
||||
}));
|
||||
app.use(cors());
|
||||
|
||||
function loadRoutes(folderName) {
|
||||
const routesPath = path.join(__dirname, folderName);
|
||||
|
@ -31,6 +27,6 @@ function loadRoutes(folderName) {
|
|||
|
||||
loadRoutes("api");
|
||||
|
||||
app.listen(80, () => {
|
||||
console.log(`Server listening on http://localhost:80/`);
|
||||
app.listen(3000, () => {
|
||||
console.log(`Server listening on http://localhost:3000/`);
|
||||
});
|
|
@ -1,21 +1,20 @@
|
|||
services:
|
||||
joclud_api:
|
||||
joclud:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
network: host
|
||||
restart: always
|
||||
container_name: joclud_api
|
||||
networks:
|
||||
- traefik
|
||||
container_name: forum
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.joclud.rule=Host(`api.joclud.leizour.fr`)"
|
||||
- "traefik.http.routers.joclud.rule=Host(`joclud.leizour.fr`)"
|
||||
- "traefik.http.routers.joclud.entrypoints=websecure"
|
||||
- "traefik.http.routers.joclud.tls=true"
|
||||
- "traefik.http.routers.joclud.tls.certresolver=myresolver"
|
||||
- "traefik.http.services.joclud.loadbalancer.server.port=80"
|
||||
- "traefik.http.services.joclud.loadbalancer.server.port=3000"
|
||||
networks:
|
||||
- traefik
|
||||
|
||||
networks:
|
||||
traefik:
|
||||
external: true
|
||||
external: true
|
21
front/.eslintrc.cjs
Normal file
21
front/.eslintrc.cjs
Normal file
|
@ -0,0 +1,21 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react/jsx-runtime',
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
||||
settings: { react: { version: '18.2' } },
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react/jsx-no-target-blank': 'off',
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
24
front/.gitignore
vendored
Normal file
24
front/.gitignore
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
8
front/README.md
Normal file
8
front/README.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
# React + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
13
front/index.html
Normal file
13
front/index.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/logo.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Joclud's Games</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
4481
front/package-lock.json
generated
Normal file
4481
front/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
32
front/package.json
Normal file
32
front/package.json
Normal file
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"name": "front",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.5.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"axios": "^1.7.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.23.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.66",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react": "^7.34.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.6",
|
||||
"vite": "^5.2.0"
|
||||
}
|
||||
}
|
7
front/public/404.html
Normal file
7
front/public/404.html
Normal file
|
@ -0,0 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="refresh" content="0; url=/" />
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
BIN
front/public/logo.png
Normal file
BIN
front/public/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.6 KiB |
27
front/src/App.jsx
Normal file
27
front/src/App.jsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { useState } from 'react';
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { fas } from '@fortawesome/free-solid-svg-icons'
|
||||
import { far } from '@fortawesome/free-regular-svg-icons';
|
||||
|
||||
|
||||
library.add(fas, far)
|
||||
|
||||
import Home from './pages/Home';
|
||||
import Login from './pages/Login';
|
||||
import Register from './pages/Register';
|
||||
import Admin from './pages/Admin';
|
||||
|
||||
export default function App() {
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/register" element={<Register />} />
|
||||
<Route path="/admin" element={<Admin />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
34
front/src/assets/Game.jsx
Normal file
34
front/src/assets/Game.jsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import Helpers from "./Helpers"
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
|
||||
export default function Game({game, token, user}) {
|
||||
function getCode(id) {
|
||||
const alphabet = 'abcdefghijklmnopqrstuvwxyz';
|
||||
const base = alphabet.length;
|
||||
let firstLetterIndex = Math.floor(id / base);
|
||||
let secondLetterIndex = id % base;
|
||||
|
||||
let firstLetter = alphabet[firstLetterIndex];
|
||||
let secondLetter = alphabet[secondLetterIndex];
|
||||
|
||||
return firstLetter + secondLetter;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='game'>
|
||||
<img src={`https://www.myludo.fr/img/jeux/1/300/${getCode(Math.floor(game.id / 1000))}/${game.id}.png`} className='game-image'/>
|
||||
<div className='game-right'>
|
||||
<h1 className='game-title'>{game.title}{game.subtitle ? `, ${game.subtitle}` : null}</h1>
|
||||
<div className="tags">
|
||||
{game.type == "extension" ? <div className="tag extension"><FontAwesomeIcon icon="fa-solid fa-plus" /> Extension</div> : <div className="tag basegame"><FontAwesomeIcon icon="fa-solid fa-puzzle-piece" /> Jeu de base</div>}
|
||||
<div className="tag duration"><FontAwesomeIcon icon="fa-regular fa-clock" /> {game.duration}</div>
|
||||
<div className="tag players"><FontAwesomeIcon icon="fa-solid fa-user-group" /> {game.players}</div>
|
||||
<div className="tag ages"><FontAwesomeIcon icon="fa-solid fa-child" /> {game.ages}</div>
|
||||
</div>
|
||||
<div className='game-bottom'>
|
||||
<Helpers gameid={game.id} user={user} token={token} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
26
front/src/assets/GameAdder.jsx
Normal file
26
front/src/assets/GameAdder.jsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import axios from 'axios';
|
||||
|
||||
export default function GameAdder({token}) {
|
||||
|
||||
|
||||
function send() {
|
||||
const file = document.getElementById("avatar");
|
||||
console.log("caca")
|
||||
console.log(file.files[0]);
|
||||
var formData = new FormData();
|
||||
formData.append("file", file.files[0])
|
||||
axios.post('/api/v1/admin/addGames', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input type="file" id="avatar" name="avatar" />
|
||||
<button onClick={send}>send</button>
|
||||
</div>
|
||||
)
|
||||
}
|
79
front/src/assets/Helpers.jsx
Normal file
79
front/src/assets/Helpers.jsx
Normal file
|
@ -0,0 +1,79 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import axios from 'axios';
|
||||
|
||||
export default function Helpers({ gameid, user, token }) {
|
||||
const [helpers, setHelpers] = useState([]);
|
||||
const [gameLoading, setGameLoading] = useState(true);
|
||||
const [helping, setHelping] = useState();
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchGame() {
|
||||
const response = await axios.post("/api/v1/games/getHelpers", { token, gameid })
|
||||
.catch((error) => console.error("Error getting game"));
|
||||
setHelpers(response.data);
|
||||
setHelping(JSON.stringify(response.data).includes(user.id));
|
||||
setGameLoading(false);
|
||||
}
|
||||
|
||||
fetchGame();
|
||||
}, [gameid, helping]);
|
||||
|
||||
function addHelper() {
|
||||
axios.post("/api/v1/games/addHelper", { token, gameid })
|
||||
.then(() => setHelping(true))
|
||||
.catch((error) => console.error("Error adding helper"));
|
||||
}
|
||||
|
||||
function removeHelper() {
|
||||
axios.post("/api/v1/games/removeHelper", { token, gameid })
|
||||
.then(() => setHelping(false))
|
||||
.catch((error) => console.error("Error removing helper"));
|
||||
}
|
||||
|
||||
function handleClick(event) {
|
||||
if (helping) {
|
||||
removeHelper();
|
||||
} else {
|
||||
addHelper();
|
||||
}
|
||||
}
|
||||
|
||||
if (gameLoading) {
|
||||
return <p>Loading...</p>
|
||||
}
|
||||
|
||||
else if (helpers.length === 0) {
|
||||
return (
|
||||
<div className='helpers'>
|
||||
<p className='no-helper'><FontAwesomeIcon icon="fa-regular fa-face-frown-open" /> Personne</p>
|
||||
<button className={`helpButton ${helping ? "helpButton-enabled" : "helpButton-disabled"}`}
|
||||
id={`helpbutton-${gameid}`}
|
||||
onClick={handleClick}>
|
||||
{helping ? <FontAwesomeIcon icon="fa-solid fa-book-bookmark" /> : <FontAwesomeIcon icon="fa-solid fa-book" />}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
else {
|
||||
return (
|
||||
<div>
|
||||
<div className='helpers'>
|
||||
{helpers.map((helper) => {
|
||||
if (helper.user_id === user.id) {
|
||||
return <p className='helper' key={helper}><FontAwesomeIcon icon="fa-regular fa-face-smile" /> Vous</p>
|
||||
} else {
|
||||
return <p className='helper' key={helper.id}><FontAwesomeIcon icon="fa-regular fa-face-smile" /> {helper.name}</p>
|
||||
}
|
||||
})}
|
||||
<button className={`helpButton ${helping ? "helpButton-enabled" : "helpButton-disabled"}`}
|
||||
id={`helpbutton-${gameid}`}
|
||||
onClick={handleClick}>
|
||||
{helping ? <FontAwesomeIcon icon="fa-solid fa-book-bookmark" /> : <FontAwesomeIcon icon="fa-solid fa-book" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
58
front/src/assets/UserVerfication.jsx
Normal file
58
front/src/assets/UserVerfication.jsx
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import axios from "axios";
|
||||
|
||||
export default function UserVerfication({token}) {
|
||||
const [users, setUsers] = useState([]);
|
||||
const [usersLoading, setUsersLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchUsers() {
|
||||
const response = await axios.post("/api/v1/admin/getUnVerifiedUsers", { token })
|
||||
.catch((error) => {console.error("Error fetching users:", error);})
|
||||
setUsers(response.data);
|
||||
setUsersLoading(false);
|
||||
}
|
||||
|
||||
fetchUsers();
|
||||
}, []);
|
||||
|
||||
function verifyUser(username) {
|
||||
axios.post("/api/v1/admin/verifyUser", { token, username })
|
||||
.then(() => {
|
||||
setUsers(users.filter((user) => user.username !== username));
|
||||
})
|
||||
.catch((error) => {console.error("Error verifying user:", error);})
|
||||
}
|
||||
|
||||
if (usersLoading) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
else if (users.length === 0) {
|
||||
return (
|
||||
<div className='users'>
|
||||
<div className='no-users'>Aucun utilisateur à vérifier</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
else {
|
||||
return (
|
||||
<div className='users'>
|
||||
{users.map((user) => {
|
||||
return (
|
||||
<div className='user' key={user.id}>
|
||||
<p className='user-info'>Nom d'utilisateur : {user.username}</p>
|
||||
<p className='user-info'>Prénom : {user.name}</p>
|
||||
<p className='user-info'>Nom : {user.lastname}</p>
|
||||
<button className='button' onClick={() => {verifyUser(user.username)}}>Vérifier</button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
0
front/src/index.css
Normal file
0
front/src/index.css
Normal file
6
front/src/main.jsx
Normal file
6
front/src/main.jsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.jsx'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
|
49
front/src/pages/Admin.css
Normal file
49
front/src/pages/Admin.css
Normal file
|
@ -0,0 +1,49 @@
|
|||
.navigation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.users {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 15px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.no-users {
|
||||
background-color: #d66666;
|
||||
padding: 7px 10px;
|
||||
border-radius: 15px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.user {
|
||||
padding: 20px;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
border-radius: 60px;
|
||||
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.5);
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
min-width: 600px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
background-color: #b4d666;
|
||||
padding: 7px 10px;
|
||||
border-radius: 15px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 664px) {
|
||||
.user {
|
||||
flex-wrap: wrap;
|
||||
padding: 10px;
|
||||
min-width: 90vw;
|
||||
border-radius: 25px;
|
||||
}
|
||||
}
|
62
front/src/pages/Admin.jsx
Normal file
62
front/src/pages/Admin.jsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import './Admin.css';
|
||||
|
||||
import UserVerfication from '../assets/UserVerfication';
|
||||
import GameAdder from '../assets/GameAdder';
|
||||
|
||||
export default function Admin() {
|
||||
const navigate = useNavigate();
|
||||
const [user, setUser] = useState();
|
||||
const [token, setToken] = useState();
|
||||
const [userloading, setUserLoading] = useState(true);
|
||||
const [section , setSection] = useState(1);
|
||||
|
||||
useEffect(() => {
|
||||
const tokenLocal = localStorage.getItem("token");
|
||||
|
||||
function checkToken() {
|
||||
if (!tokenLocal) {
|
||||
navigate("/login");
|
||||
return;
|
||||
}
|
||||
|
||||
if (JSON.parse(atob(tokenLocal.split(".")[1])).expiration < Date.now()) {
|
||||
localStorage.removeItem("token");
|
||||
navigate("/login");
|
||||
return;
|
||||
}
|
||||
|
||||
setToken(tokenLocal);
|
||||
setUser(JSON.parse(atob(tokenLocal.split(".")[1])).user);
|
||||
setUserLoading(false);
|
||||
}
|
||||
|
||||
checkToken();
|
||||
}, []);
|
||||
|
||||
if (userloading) return <div>Loading...</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className='page-title'>Base de données des jeux Joclud</h1>
|
||||
<div className='home'>
|
||||
<h2 className='welcome'>Bienvenue, {user.name} !</h2>
|
||||
<button onClick={() => navigate("/")} className='button admin-button'>Home</button>
|
||||
<button onClick={() => {
|
||||
localStorage.removeItem("token");
|
||||
navigate("/login");
|
||||
}} className='logout-button'>
|
||||
Me déconnecter
|
||||
</button>
|
||||
</div>
|
||||
<div className='navigation'>
|
||||
<button className='button' onClick={() => {setSection(1)}}>Vérification d'utilisateurs</button>
|
||||
<button className='button' onClick={() => {setSection(2)}}>Importation de jeux</button>
|
||||
</div>
|
||||
{section === 1 ? <UserVerfication token={token} /> : null}
|
||||
{section === 2 ? <GameAdder token={token} /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
204
front/src/pages/Home.css
Normal file
204
front/src/pages/Home.css
Normal file
|
@ -0,0 +1,204 @@
|
|||
body {
|
||||
text-align: center;
|
||||
background-color: #66c6d6;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 50px;
|
||||
}
|
||||
|
||||
.home {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.logout-button {
|
||||
padding: 7px 10px;
|
||||
border-radius: 15px;
|
||||
border: none;
|
||||
background-color: #d45555;
|
||||
}
|
||||
|
||||
.search {
|
||||
margin: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
padding: 7px 10px;
|
||||
border-radius: 15px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.pagination-button {
|
||||
padding: 7px 10px;
|
||||
border-radius: 15px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.games {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.game {
|
||||
padding: 30px;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
border-radius: 60px;
|
||||
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.5);
|
||||
justify-content: space-between;
|
||||
width: 900px;
|
||||
}
|
||||
|
||||
.game-image {
|
||||
width: 250px;
|
||||
margin-left: 20px;
|
||||
margin-right: 5px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.game-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
width: 100%;
|
||||
margin: 15px;
|
||||
}
|
||||
|
||||
.game-title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.game-bottom {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
background-color: #b4d666;
|
||||
padding: 7px 10px;
|
||||
border-radius: 15px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.basegame {
|
||||
background-color: #66d6bf;
|
||||
}
|
||||
|
||||
.extension {
|
||||
background-color: #e99731;
|
||||
}
|
||||
|
||||
.players {
|
||||
background-color: #fae294;
|
||||
}
|
||||
|
||||
.duration {
|
||||
background-color: #c892c4;
|
||||
}
|
||||
|
||||
.ages {
|
||||
background-color: #92c8b5;
|
||||
}
|
||||
|
||||
.helpers {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.helper {
|
||||
background-color: #b4d666;
|
||||
padding: 7px 10px;
|
||||
border-radius: 15px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.no-helper {
|
||||
background-color: #d66666;
|
||||
padding: 7px 10px;
|
||||
border-radius: 15px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.helpButton {
|
||||
padding: 7px 10px;
|
||||
border-radius: 15px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.helpButton-enabled {
|
||||
background-color: #8caf39;
|
||||
}
|
||||
|
||||
.helpButton-disabled {
|
||||
background-color: #d45555;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 664px) {
|
||||
.games {
|
||||
margin: 15px;
|
||||
}
|
||||
|
||||
.game {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border-radius: 35px;
|
||||
}
|
||||
|
||||
.game-image {
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
.game-title {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.tags{
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.helpers {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.helper {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.no-helper {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
140
front/src/pages/Home.jsx
Normal file
140
front/src/pages/Home.jsx
Normal file
|
@ -0,0 +1,140 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import axios from "axios";
|
||||
|
||||
import './Home.css';
|
||||
|
||||
import Game from '../assets/Game';
|
||||
|
||||
export default function Home() {
|
||||
const navigate = useNavigate();
|
||||
const [user, setUser] = useState();
|
||||
const [token, setToken] = useState();
|
||||
const [games, setGames] = useState([]);
|
||||
const [userloading, setUserLoading] = useState(true);
|
||||
const [gamesLoading, setGamesLoading] = useState(true);
|
||||
const [name, setName] = useState("");
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
useEffect(() => {
|
||||
const tokenLocal = localStorage.getItem("token");
|
||||
|
||||
function checkToken() {
|
||||
if (!tokenLocal) {
|
||||
navigate("/login");
|
||||
return;
|
||||
}
|
||||
|
||||
if (JSON.parse(atob(tokenLocal.split(".")[1])).expiration < Date.now()) {
|
||||
localStorage.removeItem("token");
|
||||
navigate("/login");
|
||||
return;
|
||||
}
|
||||
|
||||
setToken(tokenLocal);
|
||||
setUser(JSON.parse(atob(tokenLocal.split(".")[1])).user);
|
||||
setUserLoading(false);
|
||||
}
|
||||
|
||||
async function fetchGames() {
|
||||
try {
|
||||
const response = await axios.post("/api/v1/games/getall", { token: tokenLocal });
|
||||
setGames(response.data);
|
||||
} catch (error) {
|
||||
console.error("Error fetching games:", error);
|
||||
} finally {
|
||||
setGamesLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
checkToken();
|
||||
fetchGames();
|
||||
}, []);
|
||||
|
||||
function handleSearchChange(event) {
|
||||
setName(event.target.value);
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
const filteredGames = games.filter((game) =>
|
||||
game.title.toLowerCase().includes(name.toLowerCase())
|
||||
);
|
||||
|
||||
const itemsPerPage = 10;
|
||||
const totalPages = Math.ceil(filteredGames.length / itemsPerPage);
|
||||
|
||||
function handlePageChange(newPage) {
|
||||
if (newPage >= 1 && newPage <= totalPages) {
|
||||
setCurrentPage(newPage);
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
function resetSearch() {
|
||||
setName("");
|
||||
}
|
||||
|
||||
const currentGames = filteredGames.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage);
|
||||
|
||||
if (userloading) {
|
||||
return <p>Loading...</p>
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className='page-title'>Base de données des jeux Joclud</h1>
|
||||
<div className='home'>
|
||||
<h2 className='welcome'>Bienvenue, {user.name} !</h2>
|
||||
{user.admin ? <button onClick={() => navigate("/admin")} className='button admin-button'>Admin</button> : null}
|
||||
<button onClick={() => {
|
||||
localStorage.removeItem("token");
|
||||
navigate("/login");
|
||||
}} className='logout-button'>
|
||||
Me déconnecter
|
||||
</button>
|
||||
</div>
|
||||
<div className='search'>
|
||||
<input type="text" value={name} onChange={handleSearchChange} className='search-input' placeholder='Chercher un jeu' />
|
||||
<button className='button' onClick={resetSearch}><FontAwesomeIcon icon="fa-solid fa-xmark" /></button>
|
||||
</div>
|
||||
<div className="pagination">
|
||||
<button onClick={() => handlePageChange(1)} disabled={currentPage === 1} className="pagination-button">
|
||||
<FontAwesomeIcon icon="fa-solid fa-angles-left" />
|
||||
</button>
|
||||
<button onClick={() => handlePageChange(currentPage - 1)} disabled={currentPage === 1} className="pagination-button">
|
||||
<FontAwesomeIcon icon="fa-solid fa-chevron-left" />
|
||||
</button>
|
||||
<span className='pagination-text'>Page {currentPage} sur {totalPages}</span>
|
||||
<button onClick={() => handlePageChange(currentPage + 1)} disabled={currentPage === totalPages} className="pagination-button">
|
||||
<FontAwesomeIcon icon="fa-solid fa-chevron-right" />
|
||||
</button>
|
||||
<button onClick={() => handlePageChange(totalPages)} disabled={currentPage === totalPages} className="pagination-button">
|
||||
<FontAwesomeIcon icon="fa-solid fa-angles-right" />
|
||||
</button>
|
||||
</div>
|
||||
{gamesLoading ? <p>Loading...</p> :
|
||||
<div className='games'>
|
||||
{currentGames.map((game) => {
|
||||
return <Game key={game.id} game={game} token={token} user={user} />
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
<div className="pagination">
|
||||
<button onClick={() => handlePageChange(1)} disabled={currentPage === 1} className="pagination-button">
|
||||
<FontAwesomeIcon icon="fa-solid fa-angles-left" />
|
||||
</button>
|
||||
<button onClick={() => handlePageChange(currentPage - 1)} disabled={currentPage === 1} className="pagination-button">
|
||||
<FontAwesomeIcon icon="fa-solid fa-chevron-left" />
|
||||
</button>
|
||||
<span className='pagination-text'>Page {currentPage} sur {totalPages}</span>
|
||||
<button onClick={() => handlePageChange(currentPage + 1)} disabled={currentPage === totalPages} className="pagination-button">
|
||||
<FontAwesomeIcon icon="fa-solid fa-chevron-right" />
|
||||
</button>
|
||||
<button onClick={() => handlePageChange(totalPages)} disabled={currentPage === totalPages} className="pagination-button">
|
||||
<FontAwesomeIcon icon="fa-solid fa-angles-right" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
23
front/src/pages/Login.css
Normal file
23
front/src/pages/Login.css
Normal file
|
@ -0,0 +1,23 @@
|
|||
body {
|
||||
text-align: center;
|
||||
background-color: #66c6d6;
|
||||
}
|
||||
|
||||
.login {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: 7px 10px;
|
||||
border-radius: 15px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 7px 10px;
|
||||
border-radius: 15px;
|
||||
border: none;
|
||||
}
|
47
front/src/pages/Login.jsx
Normal file
47
front/src/pages/Login.jsx
Normal file
|
@ -0,0 +1,47 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { useNavigate, Link } from "react-router-dom";
|
||||
import axios from 'axios';
|
||||
|
||||
import './Login.css';
|
||||
|
||||
export default function Login() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (localStorage.getItem("token")) {
|
||||
navigate("/")
|
||||
}
|
||||
}, []);
|
||||
|
||||
async function login() {
|
||||
const username = document.getElementById("username").value;
|
||||
const password = document.getElementById("password").value;
|
||||
|
||||
if (!username || !password) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await axios.post("/api/v1/auth/login", { username: username, password: password})
|
||||
.catch((error) => {
|
||||
if (error.response.data.error == "you need to be verified to login") {
|
||||
alert("Vous devez être vérifié pour vous connecter, veuillez attendre qu'un administrateur vérifie votre compte.");
|
||||
} else {
|
||||
alert("Nom d'utilisateur ou mot de passe incorrect.");
|
||||
}
|
||||
});
|
||||
|
||||
localStorage.setItem("token", response.data.token);
|
||||
navigate("/");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='login'>
|
||||
<h1>Connexion</h1>
|
||||
<input type="text" id="username" className='input' placeholder="Nom d'utilisateur"/>
|
||||
<input type="password" id="password" className='input' placeholder='Mot de passe'/>
|
||||
<button onClick={login} className='button'>Me connecter</button>
|
||||
<Link to="/register">Créer un compte</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
23
front/src/pages/Register.css
Normal file
23
front/src/pages/Register.css
Normal file
|
@ -0,0 +1,23 @@
|
|||
body {
|
||||
text-align: center;
|
||||
background-color: #66c6d6;
|
||||
}
|
||||
|
||||
.register {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: 7px 10px;
|
||||
border-radius: 15px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 7px 10px;
|
||||
border-radius: 15px;
|
||||
border: none;
|
||||
}
|
53
front/src/pages/Register.jsx
Normal file
53
front/src/pages/Register.jsx
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { useNavigate, Link } from "react-router-dom";
|
||||
import axios from 'axios';
|
||||
|
||||
import './Register.css';
|
||||
|
||||
export default function Register() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (localStorage.getItem("token")) {
|
||||
navigate("/")
|
||||
}
|
||||
}, []);
|
||||
|
||||
async function register() {
|
||||
const username = document.getElementById("username").value;
|
||||
const name = document.getElementById("name").value;
|
||||
const lastname = document.getElementById("lastname").value;
|
||||
const password = document.getElementById("password").value;
|
||||
|
||||
if (!username || !name || !lastname || !password) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post("/api/v1/auth/register", {
|
||||
username: username,
|
||||
name: name,
|
||||
lastname: lastname,
|
||||
password: password
|
||||
})
|
||||
navigate("/login");
|
||||
} catch (error) {
|
||||
alert("Username already exists");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='register'>
|
||||
<h1>Création de compte</h1>
|
||||
<input type="text" id="username" placeholder="Nom d'utilisateur" className='input'/>
|
||||
<input type="text" id="name" placeholder='Prénom' className='input'/>
|
||||
<input type="text" id="lastname" placeholder='Nom de famille' className='input'/>
|
||||
<input type="password" id="password" placeholder='Mot de passe' className='input'/>
|
||||
<div>
|
||||
<button onClick={register} className='button'>Créer un compte</button>
|
||||
</div>
|
||||
<Link to="/login">Me connecter avec mon compte</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
17
front/vite.config.js
Normal file
17
front/vite.config.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3000',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
ws: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
Loading…
Add table
Add a link
Reference in a new issue