22 Commits

Author SHA1 Message Date
98290b41a1 Merge branch 'GringoElPepito' of https://git.justw.tf/Lightemerald/nuitdelinfo2023 into GringoElPepito 2023-12-08 07:21:10 +01:00
3660e1cd7c Following 2023-12-08 07:21:06 +01:00
8c184944f3 styling game page 2023-12-08 07:20:06 +01:00
49bc4f64fb Starting Quizz 2023-12-08 05:43:14 +01:00
36f5e7dfa6 Merge remote-tracking branch 'origin/GringoElPepito' into GringoElPepito 2023-12-08 05:23:06 +01:00
5669bb6be3 styling the home page 2023-12-08 05:22:33 +01:00
c49faca15b Following game 2023-12-08 03:52:15 +01:00
08a8819d25 Merge branch 'GringoElPepito' of https://git.justw.tf/Lightemerald/nuitdelinfo2023 into GringoElPepito 2023-12-08 02:37:22 +01:00
d2a8189a6b Animation transition game 2023-12-08 02:37:19 +01:00
9ae801fea9 fix username problem 2023-12-08 01:57:36 +01:00
9bcded7f9a adding logout and show the username 2023-12-08 00:50:36 +01:00
80f786ff61 adding register and login pages 2023-12-08 00:08:21 +01:00
ddfdcd613f Adding button redirection
Following game
2023-12-08 00:03:39 +01:00
95eec4f93e Starting Game MainMenu 2023-12-07 22:57:26 +01:00
590b6773f8 Starting home page 2023-12-07 21:40:34 +01:00
bca2ca59e6 Merge branch 'GringoElPepito' of https://git.justw.tf/Lightemerald/nuitdelinfo2023 into GringoElPepito 2023-12-07 20:09:54 +01:00
9b6403f9a5 Splitting navbarre component 2023-12-07 20:09:48 +01:00
a441c3e78b adding Router and starting the login page 2023-12-07 20:09:33 +01:00
6a91e0739a Starting navabarre 2023-12-07 19:42:51 +01:00
a9ed1691f2 adding tailwind & flowbite integration 2023-12-07 19:04:53 +01:00
c4ce652ab3 Structure react app 2023-12-07 18:45:39 +01:00
5a5600c375 Start Webapp 2023-12-07 18:34:57 +01:00
70 changed files with 19032 additions and 1007 deletions

View File

@@ -1,50 +0,0 @@
{
"extends": "eslint:recommended",
"env": {
"node": true,
"es6": true
},
"parserOptions": {
"ecmaVersion": 2021,
"sourceType": "module"
},
"rules": {
"arrow-spacing": ["warn", { "before": true, "after": true }],
"brace-style": ["error", "stroustrup", { "allowSingleLine": true }],
"comma-dangle": ["error", "always-multiline"],
"comma-spacing": "error",
"comma-style": "error",
"curly": ["error", "multi-line", "consistent"],
"dot-location": ["error", "property"],
"handle-callback-err": "off",
"indent": ["error", "tab"],
"keyword-spacing": "error",
"max-nested-callbacks": ["error", { "max": 4 }],
"max-statements-per-line": ["error", { "max": 2 }],
"no-console": "off",
"no-empty-function": "error",
"no-floating-decimal": "error",
"no-inline-comments": "error",
"no-lonely-if": "error",
"no-multi-spaces": "error",
"no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1, "maxBOF": 0 }],
"no-shadow": ["error", { "allow": ["err", "resolve", "reject"] }],
"no-trailing-spaces": ["error"],
"no-var": "error",
"object-curly-spacing": ["error", "always"],
"prefer-const": "error",
"quotes": ["error", "single"],
"semi": ["error", "always"],
"space-before-blocks": "error",
"space-before-function-paren": ["error", {
"anonymous": "never",
"named": "never",
"asyncArrow": "always"
}],
"space-in-parens": "error",
"space-infix-ops": "error",
"space-unary-ops": "error",
"spaced-comment": "error",
"yoda": "error"
}
}

View File

@@ -1,6 +0,0 @@
DATABASE_HOST="127.0.0.1"
DATABASE_NAME=nuitdelinfo2023
DATABASE_USER=nuitdelinfo2023
DATABASE_PASSWORD=""
JWT_SECRET=""
PORT=3000

177
api/.gitignore vendored
View File

@@ -1,177 +0,0 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Caches
.cache
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store
# Level
tokensDB/

View File

@@ -1,133 +0,0 @@
import { pool } from '../modules/database.js';
class Game {
constructor(id = null, playerId) {
this.id = id;
this.player = playerId;
this.questions = [];
}
async get() {
try {
const [rows] = await pool.execute(
'SELECT * FROM games WHERE id = ? AND player = ? LIMIT 1', [this.id, this.player],
);
if (!rows.length) return false;
const [questions] = await pool.execute(
'SELECT * FROM games_questions WHERE game = ?', [this.id],
);
questions.forEach(q => {
const question = new Question(q.id, q.question);
this.questions.push(question);
});
return true;
}
catch (error) {
console.error(error);
return false;
}
}
async create() {
try {
const [rows] = await pool.execute(
'INSERT INTO games (player) VALUES (?)', [this.player],
);
this.id = rows.insertId;
return true;
}
catch (error) {
console.error(error);
return false;
}
}
async generateQuestions(themeId) {
try {
const [rows] = await pool.execute(
'SELECT * FROM questions WHERE theme = ? ORDER BY RAND() LIMIT 10', [themeId],
);
rows.forEach(async row => {
const question = new Question(row.id, row.question);
this.questions.push(question);
await pool.execute('INSERT INTO games_questions (game, question) VALUES (?, ?)', [this.id, row.id]);
});
return true;
}
catch (error) {
console.error(error);
return false;
}
}
}
class Question {
constructor(id = null, question = null) {
this.id = id;
this.question = question;
this.answers = [];
}
async get() {
try {
const [rows] = await pool.execute(
'SELECT * FROM questions WHERE id = ? LIMIT 1', [this.id],
);
if (!rows.length) return false;
this.question = rows[0].question;
const [answers] = await pool.execute(
'SELECT * FROM answers WHERE question = ?', [this.id],
);
answers.forEach(a => {
const answer = new Answer(a.id, a.answer, a.correct);
this.answers.push(answer);
});
return true;
}
catch (error) {
console.error(error);
return false;
}
}
async fetchAnswers() {
try {
const [rows] = await pool.execute(
'SELECT * FROM answers WHERE question = ?', [this.id],
);
rows.forEach(row => {
const answer = new Answer(row.id, row.answer, row.correct);
this.answers.push(answer);
});
return true;
}
catch (error) {
console.error(error);
return false;
}
}
async verifyAnswer(answerId) {
try {
const [rows] = await pool.execute(
'SELECT * FROM answers WHERE question = ? AND id = ? AND correct = 1', [this.id, answerId],
);
if (!rows.length) return false;
return true;
}
catch (error) {
console.error(error);
return false;
}
}
}
class Answer {
constructor(id = null, answer = null, correct = null) {
this.id = id;
this.answer = answer;
this.correct = correct;
}
}
export { Game, Question, Answer };

View File

@@ -1,13 +0,0 @@
# nuitdelinfo2023-api
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.js
```

Binary file not shown.

View File

@@ -1,148 +0,0 @@
SET default_storage_engine = InnoDB;
DROP DATABASE IF EXISTS `nuitdelinfo2023`;
CREATE DATABASE IF NOT EXISTS `nuitdelinfo2023`
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
DROP USER IF EXISTS 'nuitdelinfo2023';
CREATE USER 'nuitdelinfo2023'@'localhost' IDENTIFIED BY 'PASSWORD';
GRANT ALL PRIVILEGES ON nuitdelinfo2023.* TO 'nuitdelinfo2023'@'localhost';
USE `nuitdelinfo2023`;
CREATE TABLE users (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
username VARCHAR(64) NOT NULL,
password VARCHAR(255) NOT NULL,
score INT UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (id),
INDEX ur_username_idx (username)
) ENGINE=InnoDB;
CREATE TABLE games (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
player INT UNSIGNED NOT NULL,
PRIMARY KEY (id),
INDEX g_player_idx (player)
) ENGINE=InnoDB;
CREATE TABLE themes (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
theme VARCHAR(255) NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB;
CREATE TABLE questions (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
theme INT UNSIGNED NOT NULL,
question VARCHAR(255) NOT NULL,
PRIMARY KEY (id),
INDEX q_theme_idx (theme)
) ENGINE=InnoDB;
CREATE TABLE answers (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
question INT UNSIGNED NOT NULL,
answer VARCHAR(255) NOT NULL,
correct BOOLEAN NOT NULL DEFAULT FALSE,
PRIMARY KEY (id),
INDEX a_question_idx (question)
) ENGINE=InnoDB;
CREATE TABLE game_questions (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
game INT UNSIGNED NOT NULL,
question INT UNSIGNED NOT NULL,
score INT UNSIGNED,
PRIMARY KEY (id),
INDEX gq_game_idx (game),
INDEX gq_question_idx (question)
) ENGINE=InnoDB;
INSERT INTO themes (theme) VALUES ('Environnement');
INSERT INTO themes (theme) VALUES ('Energie Renouvelable');
INSERT INTO themes (theme) VALUES ("Recyclage et consommation d'energie");
INSERT INTO themes (theme) VALUES ('Pollution et energie fossile');
INSERT INTO themes (theme) VALUES ('Politique');
INSERT INTO questions (id, theme, question)
VALUES
(1, 3, 'Il vaut mieux jeter les cendres de sa cheminée'),
(2, 2, 'Dans le nord, les panneaux solaires ne sont pas efficaces'),
(3, 3, 'Il vaut mieux faire la vaisselle à la main'),
(4, 4, 'Le diesel pollue moins que l''essence'),
(5, 3, 'Utiliser des baguettes jetables en bois, c''est écolo'),
(6, 4, 'L''encens nassaini pas l''air'),
(7, 4, 'En ville, il vaut mieux ouvrir ses fenêtres'),
(8, 1, 'Les gratte-ciels, c''est mauvais pour l''environnement'),
(9, 1, 'les maisons en bois brûlent plus facilement'),
(10, 1, 'Les produits écologiques sont plus chers'),
(11, 4, 'Les jardiniers polluent plus que les agriculteurs'),
(12, 1, 'Le lino, c''est un revêtment mauvais pour l''environnement'),
(13, 4, 'Le charbon ne représente plus grand chose'),
(14, 1, 'Les animaux domestiquent nuisent à l''environnement'),
(15, 1, 'Les produits bio sont forcément écologiques'),
(16, 4, 'ne pas laver sa voiture pollue moins que la laver régulièrement'),
(17, 2, 'les panneaux solaires se valent tous'),
(18, 3, 'il vaut mieux consommer l''électricité qu''on produit soi-même que l''acheter à EDF'),
(19, 1, 'Les vêtements en coton, c''est écolo'),
(20, 4, 'Utiliser du papier favorise la déforestation'),
(21, 4, 'Lair est plus pollué en ville quà la campagne'),
(22, 1, 'Un sapin artificiel, c''est mieux pour l''environnement'),
(23, 1, 'Il n''y a pas de produit vraiment bio'),
(24, 1, 'On peut utiliser n''importe quel bois, c''est toujours écologique'),
(25, 2, 'Les éoliennes tuent les oiseaux et font du bruit'),
(26, 4, 'Ne pas utiliser de bouchons en liège, c''est mieux pour la nature'),
(27, 2, 'les panneaux solaires ne sont pas rentables du fait d''une durée de vie trop courte'),
(28, 1, 'un produit sans sucre ne contient pas de sucre'),
(29, 3, 'Il faut laver la vaisselle à l''eau chaude'),
(30, 2, 'les éoliennes produisent plus en été'),
(31, 4, 'en hiver, il vaut mieux moins aérer'),
(32, 3, 'Appareils électroniques en veille'),
(33, 5, 'la politique internationale a du mal à atteindre des accords significatifs sur les changements climatiques'),
(34, 2, 'les panneaux solaires se recyclent mal'),
(35, 1, 'Il est impossible de nourrir toute la planète avec une agriculture bio.'),
(36, 4, 'La voiture électrique va sauver la planète.'),
(37, 2, 'Les énergies renouvelables (ENR) sont plus chères à produire.'),
(38, 4, 'On ne pourra pas atteindre la neutralité carbone sans le nucléaire en France à lhorizon 2050.'),
(39, 3, 'Le numérique lutte contre le changement climatique en réduisant la consommation dénergie.'),
(40, 4, 'Les mécanismes de compensation carbone sont du greenwashing et ne servent à rien.'),
(41, 3, 'Adopter un mode de vie écolo, ce serait revenir au Moyen Âge.'),
(42, 4, 'Si les individus adoptent des gestes responsables, cela ne changera rien, car cest lactivité économique qui pollue.'),
(43, 4, 'La France est une goutte deau : ce sont la Chine et lInde qui émettent les plus grandes quantités de gaz à effet de serre.'),
(44, 5, 'politiques environnementales influencées'),
(45, 5, 'accords internationaux sur le climat'),
(46, 5, 'politiques de tarification du carbone'),
(47, 5, 'politiques environnementales.'),
(48, 5, 'Actions individuelles'),
(49, 3, 'Intérêt du recyclage'),
(50, 4, 'Quel est le principal gaz à effet de serre responsable du réchauffement climatique ?'),
(51, 3, 'eau du robinet ou eau en bouteille'),
(52, 2, 'viabilité de lénergie solaire et éolienne'),
(53, 1, 'Quelle est la solution la plus efficace pour lutter contre le réchauffement climatique ?'),
(54, 5, 'Quel est le nom du célèbre ours polaire qui a ému le monde entier en apparaissant amaigri et affamé sur la banquise ?'),
(55, 4, 'Quel est le pays qui émet le plus de gaz à effet de serre par habitant ?'),
(56, 5, 'Quel est le nom du mouvement de jeunesse qui organise des grèves scolaires pour le climat ?'),
(57, 5, 'Quel est le nom du traité international qui vise à limiter le réchauffement climatique à 2°C dici 2100 ?'),
(58, 2, 'Efficacite des energies renouvelables'),
(59, 5, 'Le réchauffement climatique, cest un complot des élites pour nous imposer leur agenda politique, non ?'),
(60, 2, 'durabilité des stockage dénergie'),
(61, 2, 'Transition energetique trop couteuse'),
(62, 5, 'les politiques nationales sur les énergies renouvelables sont cruciales pour atteindre les objectifs climatiques mondiaux'),
(63, 4, 'Les émissions de CO2 sont le principal facteur du réchauffement climatique et doivent être réduites au maximum.'),
(64, 1, 'Laugmentation de la température moyenne de la planète est due à un cycle naturel et na rien à voir avec les activités humaines.'),
(65, 2, 'Les énergies renouvelables sont trop chères et pas assez efficaces pour remplacer les énergies fossiles.'),
(66, 1, 'La biodiversité nest pas menacée, il y a toujours autant despèces vivantes sur la planète.'),
(67, 3, 'Le recyclage est la meilleure solution pour réduire les déchets et préserver les ressources naturelles.'),
(68, 1, 'Les produits biologiques sont meilleurs pour lenvironnement que les produits conventionnels.'),
(69, 4, 'Les voitures électriques sont plus écologiques que les voitures à essence ou diesel.'),
(70, 4, 'Le plastique est le pire ennemi de lenvironnement et il faut léliminer complètement.'),
(71, 1, 'Le réchauffement climatique est causé par les activités humaines.'),
(72, 1, 'La fonte des glaces polaires va faire monter le niveau des océans et submerger les zones côtières.'),
(73, 2, 'Les énergies renouvelables sont plus propres et plus efficaces que les énergies fossiles.'),
(74, 1, 'La déforestation est un problème mineur qui naffecte que quelques régions du monde.'),
(75, 3, 'Le recyclage est une solution efficace pour réduire les déchets et préserver les ressources naturelles.'),
(76, 1, 'Lagriculture biologique est plus respectueuse de lenvironnement et de la santé que lagriculture conventionnelle.'),
(77, 5, 'politiques environnementales strictes'),
(78, 1, 'Les végétariens et les végétaliens sont plus respectueux de lenvironnement et de la santé que les omnivores.');

View File

@@ -1,39 +0,0 @@
import fs from 'fs';
import path from 'path';
import cors from 'cors';
import logger from 'morgan';
import express from 'express';
import { log } from './modules/log.js';
import { speedLimiter } from './modules/requestHandler.js';
import testRouter from './routes/test.js';
import usersRouter from './routes/users.js';
import leaderboardRouter from './routes/leaderboard.js';
import themeRouter from './routes/themes.js';
import gameRouter from './routes/games.js';
const app = express();
app.set('trust proxy', 1);
app.use(express.json());
app.use(logger('dev'));
app.use(speedLimiter);
app.use(logger('combined', { stream: fs.createWriteStream(path.join(__dirname, 'logs/access.log'), { flags: 'a' }) }));
app.use(cors({
origin: '*',
}));
app.use(express.static('public'));
// routes
app.use('/api/test', testRouter);
app.use('/api/users', usersRouter);
app.use('/api/leaderboard', leaderboardRouter);
app.use('/api/themes', themeRouter);
app.use('/api/games', gameRouter);
// run the API
app.listen(process.env.PORT, async () => {
log(`running at port ${process.env.PORT}`);
});

View File

@@ -1,22 +0,0 @@
{
"compilerOptions": {
"lib": ["ESNext"],
"module": "esnext",
"target": "esnext",
"moduleResolution": "bundler",
"moduleDetection": "force",
"allowImportingTsExtensions": true,
"noEmit": true,
"composite": true,
"strict": true,
"downlevelIteration": true,
"skipLibCheck": true,
"jsx": "react-jsx",
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"allowJs": true,
"types": [
"bun-types" // add Bun global
]
}
}

View File

@@ -1,26 +0,0 @@
import mysql from 'mysql2';
const connection = mysql.createConnection({
host: process.env.DATABASE_HOST,
user: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
});
const pool = mysql.createPool({
host: process.env.DATABASE_HOST,
user: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
}).promise();
function createPool(host, user, password, db) {
const newPool = mysql.createPool({
host: host,
user: user,
password: password,
database: db,
}).promise();
return newPool;
}
export { connection, pool, createPool };

View File

@@ -1,19 +0,0 @@
import pino from 'pino';
const logger = pino();
export function log(x) {
logger.info(x);
}
export function debug(x) {
logger.debug(x);
}
export function warn(x) {
logger.warn(x);
}
export function error(x) {
logger.error(x);
}

View File

@@ -1,20 +0,0 @@
export function random(x, y) {
return crypto.randomInt(x, y + 1);
}
export function random2(min, max) {
const range = max - min + 1;
const byteLength = Math.ceil(Math.log2(range) / 8);
let randomValue;
do {
const randomBytes = crypto.randomBytes(byteLength);
randomValue = parseInt(randomBytes.toString('hex'), 16);
} while (randomValue >= range);
return randomValue + min;
}
export function randomHEX(x) {
return crypto.randomBytes(x).toString('hex');
}

View File

@@ -1,54 +0,0 @@
import rateLimit from 'express-rate-limit';
import slowDown from 'express-slow-down';
import http from 'http';
import os from 'os';
const requestLimiter = rateLimit({
windowMs: 60 * 1000,
max: 5,
standardHeaders: true,
legacyHeaders: false,
message: 'Too many requests from this IP, please try again later',
});
const speedLimiter = slowDown({
windowMs: 60 * 1000,
delayAfter: 5,
delayMs: (hits) => hits * 100,
});
function checkSystemLoad(req, res, next) {
const load = os.loadavg()[0];
const cores = os.cpus().length;
const threshold = cores * 0.7;
if (load > threshold) {
return respondWithStatus(res, 503, 'API Unavailable - System Overloaded!');
}
return next();
}
function respondWithStatus(res, statusCode, message) {
const response = { status: statusCode, message: message };
if (statusCode >= 400 && statusCode <= 599) {
response.error = http.STATUS_CODES[statusCode];
}
return res.status(statusCode).json(response);
}
function respondWithStatusJSON(res, statusCode, JSON) {
const response = { status: statusCode, JSON };
if (statusCode >= 400 && statusCode <= 599) {
response.error = http.STATUS_CODES[statusCode];
}
return res.status(statusCode).json(response);
}
export {
requestLimiter,
speedLimiter,
checkSystemLoad,
respondWithStatus,
respondWithStatusJSON,
};

View File

@@ -1,39 +0,0 @@
/* eslint-disable no-undef */
import jwt from 'jsonwebtoken';
import { respondWithStatus } from './requestHandler.js';
import { pool } from './database.js';
// Generate a new JWT
const generateToken = async (userId, password) => {
return await jwt.sign({ userId: userId, password: password }, process.env.JWT_SECRET, { expiresIn: '7d' });
};
// Middleware to verify the JWT and set req.userId
const verifyToken = async (req, res, next) => {
const token = req.headers.authorization;
if (!token) return await respondWithStatus(res, 401, 'No token provided');
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.userId = decoded.userId;
const [rows] = await pool.execute(
'SELECT * FROM users WHERE id = ? LIMIT 1', [req.userId],
);
if (!rows.length) return await respondWithStatus(res, 404, 'User not found!');
const passwordMatch = await Bun.password.verify(decoded.password, rows[0].password);
if (!passwordMatch) return await respondWithStatus(res, 401, 'Token is invalid');
const now = Date.now().valueOf() / 1000;
if (decoded.exp - now <= 0) {
return await respondWithStatus(res, 401, 'Token is invalid');
}
req.username = rows[0].username;
next();
}
catch (error) {
return await respondWithStatus(res, 401, 'Invalid user');
}
};
export { generateToken, verifyToken };

View File

@@ -1,22 +1,11 @@
{
"name": "nuitdelinfo2023-api",
"module": "index.js",
"type": "module",
"devDependencies": {
"bun-types": "latest",
"eslint": "^8.55.0"
"name": "nuit-info-2023-api",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"cors": "^2.8.5",
"express": "^4.18.2",
"express-rate-limit": "^7.1.5",
"express-slow-down": "^2.0.1",
"jsonwebtoken": "^9.0.2",
"morgan": "^1.10.0",
"mysql2": "^3.6.5",
"pino": "^8.16.2"
}
}
"author": "",
"license": "ISC"
}

View File

@@ -1,44 +0,0 @@
import express from 'express';
import { pool } from '../modules/database.js';
import { verifyToken } from '../modules/token.js';
import { respondWithStatus, respondWithStatusJSON } from '../modules/requestHandler.js';
import { Game, Question } from '../Classes/Games.js';
const router = express.Router();
router.post('/create/:theme', verifyToken, async (req, res) => {
const game = new Game(null, req.userId);
await game.create();
await game.generateQuestions(req.params.theme);
return await respondWithStatusJSON(res, 200, {
message: 'Successfully created game',
game,
});
});
router.post('/verify/:game', verifyToken, async (req, res) => {
const [rows] = await pool.execute(
'SELECT * FROM games WHERE id = ? AND player = ? LIMIT 1', [req.params.game, req.userId],
);
if (!rows.length) return await respondWithStatus(res, 404, 'Game not found');
const { question, answer } = req.body;
if (![question, answer].every(Boolean)) return await respondWithStatus(res, 400, 'Missing fields');
const q = new Question(question);
const [gameQuestions] = await pool.execute('SELECT * FROM game_questions WHERE game = ? AND question = ?', [req.params.game, question]);
if (!gameQuestions.length) return await respondWithStatus(res, 404, 'Question not found');
if (gameQuestions[0].score) return await respondWithStatus(res, 400, 'Question already answered');
if (q.verifyAnswer(answer)) {
await pool.execute('UPDATE game_questions SET score = 1 WHERE game = ? AND question = ?', [req.params.game, question]);
res.status(200).json({
message: 'Answer is correct',
});
}
else {
res.status(200).json({
message: 'Answer is incorrect',
});
}
});
export default router;

View File

@@ -1,40 +0,0 @@
/* eslint-disable no-undef */
import express from 'express';
import { pool } from '../modules/database.js';
import { requestLimiter, respondWithStatus, respondWithStatusJSON } from '../modules/requestHandler.js';
const router = express.Router();
router.get('/', requestLimiter, async (req, res) => {
try {
const [rows] = await pool.execute('SELECT * FROM users');
if (!rows.length) return await respondWithStatus(res, 404, 'There are no users');
return await respondWithStatusJSON(res, 200, {
message: 'Successfully retrieved users',
users: rows,
});
}
catch (error) {
console.error(error);
return await respondWithStatus(res, 500, 'An error has occured');
}
});
router.get('/:username', requestLimiter, async (req, res) => {
try {
const [rows] = await pool.execute('SELECT u.*, (SELECT COUNT(*) + 1 FROM users AS uu WHERE uu.score > u.score) AS rank FROM users AS u WHERE username = ? LIMIT 1', [req.params.username]);
if (!rows.length) return await respondWithStatus(res, 404, 'There are no users');
return await respondWithStatusJSON(res, 200, {
message: 'Successfully retrieved user',
users: rows[0],
});
}
catch (error) {
console.error(error);
return await respondWithStatus(res, 500, 'An error has occured');
}
});
export default router;

View File

@@ -1,35 +0,0 @@
import express from 'express';
const router = express.Router();
router.get('/', (req, res) => {
res.status(200).json({ code: 200, message:'Received GET request from ip:' + req.ip });
});
router.post('/', (req, res) => {
res.status(200).json({ code: 200, message:'Received POST request' });
});
router.patch('/', (req, res) => {
res.status(200).json({ code: 200, message:'Received PUT request' });
});
router.put('/', (req, res) => {
res.status(200).json({ code: 200, message:'Received PUT request' });
});
router.delete('/', (req, res) => {
res.status(200).json({ code: 200, message:'Received DELETE request' });
});
export default router;

View File

@@ -1,27 +0,0 @@
import express from 'express';
import { verifyToken } from '../modules/token.js';
import { respondWithStatus, respondWithStatusJSON } from '../modules/requestHandler.js';
import { pool } from '../modules/database.js';
const router = express.Router();
// send list of themes
router.get('/', verifyToken, async (req, res) => {
const [rows] = await pool.execute('SELECT * FROM themes');
if (!rows.length) return await respondWithStatus(res, 404, 'There are no themes');
return await respondWithStatusJSON(res, 200, {
message: 'Successfully retrieved themes',
themes: rows,
});
});
router.get('/:id', verifyToken, async (req, res) => {
const [rows] = await pool.execute('SELECT * FROM themes WHERE id = ? LIMIT 1', [req.params.id]);
if (!rows.length) return await respondWithStatus(res, 404, 'Theme not found');
return await respondWithStatusJSON(res, 200, {
message: 'Successfully retrieved theme',
themes: rows[0],
});
});
export default router;

View File

@@ -1,95 +0,0 @@
/* eslint-disable no-undef */
import express from 'express';
import jwt from 'jsonwebtoken';
import { pool } from '../modules/database.js';
import { generateToken } from '../modules/token.js';
import { requestLimiter, respondWithStatus, respondWithStatusJSON } from '../modules/requestHandler.js';
const router = express.Router();
router.post('/register', requestLimiter, async (req, res) => {
const { username, password } = req.body;
if ([ username, password ].every(Boolean)) {
try {
const [existingUsername] = await pool.execute('SELECT * FROM users WHERE username = ? LIMIT 1', [username]);
if (existingUsername.length) return await respondWithStatus(res, 400, 'Username is already taken');
const hashedPassword = await Bun.password.hash(password);
const [result] = await pool.execute('INSERT INTO users (username, password) VALUES (?, ?)', [ username, hashedPassword ]);
if (result.affectedRows === 0) return await respondWithStatus(res, 500, 'Error storing user');
const user = await pool.execute('SELECT * FROM users WHERE username = ? LIMIT 1', [ username ]);
const token = await generateToken(user[0].id, password);
return await respondWithStatusJSON(res, 200, { message: 'Successfully registered', token: token, username: username });
}
catch (error) {
console.error(error);
return await respondWithStatus(res, 500, 'An error has occured');
}
}
else {
return await respondWithStatus(res, 400, 'Missing fields');
}
});
router.post('/login', requestLimiter, async (req, res) => {
const { username, password } = req.body;
if ([username, password].every(Boolean)) {
try {
const [rows] = await pool.execute(
'SELECT * FROM users WHERE username = ? LIMIT 1', [username],
);
if (!rows.length) return await respondWithStatus(res, 404, 'Incorrect username or email');
const user = rows[0];
const passwordMatch = await Bun.password.verify(password, user.password);
if (!passwordMatch) return await respondWithStatus(res, 401, 'Incorrect password');
const token = await generateToken(user.id, password);
return await respondWithStatusJSON(res, 200, {
message: 'Login successful',
token: token,
user: {
id: user.id,
username: user.username,
},
});
}
catch (error) {
console.error(error);
return await respondWithStatus(res, 500, 'An error has occured');
}
}
else {
return await respondWithStatus(res, 400, 'Missing fields');
}
});
router.post('/verify', requestLimiter, async (req, res) => {
const token = req.headers.authorization || req.body.token;
if (!token) return await respondWithStatus(res, 401, 'No token provided');
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const [rows] = await pool.execute(
'SELECT * FROM users WHERE id = ? LIMIT 1', [decoded.userId],
);
if (!rows.length) return await respondWithStatus(res, 404, 'User not found!');
const passwordMatch = await Bun.password.verify(decoded.password, rows[0].password);
if (!passwordMatch) return await respondWithStatus(res, 401, 'Token is invalid');
const now = Date.now().valueOf() / 1000;
if (decoded.exp - now <= 0) {
return await respondWithStatus(res, 401, 'Token is invalid');
}
return await respondWithStatusJSON(res, 200, {
message: 'Token is valid',
user: {
id: rows[0].id,
username: rows[0].username,
},
});
}
catch (error) {
return await respondWithStatus(res, 401, 'Invalid user');
}
});
export default router;

23
webapp/.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

70
webapp/README.md Normal file
View File

@@ -0,0 +1,70 @@
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
The page will reload when you make changes.\
You may also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `npm run build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)

18063
webapp/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

43
webapp/package.json Normal file
View File

@@ -0,0 +1,43 @@
{
"name": "nuit-info-2023-webapp",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"flowbite": "^2.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^13.5.0",
"react-router-dom": "^6.20.1",
"react-scripts": "5.0.1",
"tailwindcss": "^3.3.6",
"translate": "^2.0.2",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

BIN
webapp/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

44
webapp/public/index.html Normal file
View File

@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/flowbite/2.2.0/flowbite.min.js"></script>
</body>
</html>

BIN
webapp/public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
webapp/public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

3
webapp/public/robots.txt Normal file
View File

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -0,0 +1,12 @@
function Ammo({arg}) {
return(
<div className="relative h-48 w-32 bottom-0 bg-white overflow-hidden transition-all duration-200 hover:bottom-full rounded-md">
<p>{arg}</p>
<div className="absolute left-d24 bottom-0 h-4 w-full rotate-45 bg-darkGreen">
</div>
</div>
);
}
export default Ammo;

View File

@@ -0,0 +1,16 @@
import Ammo from "../ammo";
import { get } from "../../../../modules/fetcher";
import { useEffect, useState } from "react";
function AmmoBox({game}) {
return(
<div className="fixed bottom-0 left-28 right-0 h-24 flex flex-row justify-between items-start">
<Ammo arg={"test"}/>
<Ammo arg={"test2"}/>
<Ammo arg={"test3"}/>
<Ammo arg={"test4"}/>
</div>
);
}
export default AmmoBox;

View File

@@ -0,0 +1,50 @@
import {useState} from "react";
import { useNavigate } from "react-router-dom";
import img from "../poseimg";
function CardMission({theme}) {
const id = theme.id;
const navigate = useNavigate();
const [validate,setValidate] = useState(false);
let transi = validate ? "right-0" : "right-dfull";
let group1 = validate ? "left-0" : "left-1/4";
let group2 = validate ? "left-1/4" : "left-1/2";
let group3 = validate ? "left-1/2" : "left-3/4";
let group4 = validate ? "left-3/4" : "left-full";
function changeTransi(id) {
setValidate(!validate);
setTimeout(() => {
navigate('/game/'+id);
}, 1000);
}
return(
<div className="h-60 w-72 bg-slate-200 flex flex-col justify-start items-center gap-5 rounded-md">
<h2 className="font-bold mt-1">{theme.theme}</h2>
<div>
<img src={img[id]} alt="perso" className="h-28 w-28 rounded-full object-cover object-left-top"/>
</div>
<button className="px-1 py-2 w-32 rounded-lg text-white bg-darkBlue mt-2" onClick={() => changeTransi(id)}>Play</button>
<div id={"transi-"+id} className={"fixed w-full h-1/2 top-1/4 py-4 bg-red-700 transition-all flex justify-center items-center "+transi}>
<img src={img[id]} alt="perso" className="h-full"/>
{/* <div className={"h-2 w-24 rounded-full bg-white blur-sm shadow shadow-white fixed top-1/3 transition duration-1000 "+group1}></div>
<div className={"h-2 w-24 rounded-full bg-white blur-sm shadow shadow-white fixed top-2/3 transition duration-1000 "+group1}></div>
<div className={"h-2 w-24 rounded-full bg-white blur-sm shadow shadow-white fixed top-1/4 transition duration-1000 "+group2}></div>
<div className={"h-2 w-24 rounded-full bg-white blur-sm shadow shadow-white fixed top-1/2 transition duration-1000 "+group2}></div>
<div className={"h-2 w-24 rounded-full bg-white blur-sm shadow shadow-white fixed top-3/4 transition duration-1000 "+group2}></div>
<div className={"h-2 w-24 rounded-full bg-white blur-sm shadow shadow-white fixed top-1/3 transition duration-1000 "+group3}></div>
<div className={"h-2 w-24 rounded-full bg-white blur-sm shadow shadow-white fixed top-2/3 transition duration-1000 "+group3}></div>
<div className={"h-2 w-24 rounded-full bg-white blur-sm shadow shadow-white fixed top-1/4 transition duration-1000 "+group4}></div>
<div className={"h-2 w-24 rounded-full bg-white blur-sm shadow shadow-white fixed top-1/2 transition duration-1000 "+group4}></div>
<div className={"h-2 w-24 rounded-full bg-white blur-sm shadow shadow-white fixed top-3/4 transition duration-1000 "+group4}></div> */}
</div>
</div>
)
}
export default CardMission;

View File

@@ -0,0 +1,30 @@
import AmmoBox from "../ammobox";
import { post } from "../../../../modules/fetcher";
import { useEffect, useState } from "react";
import img from "../poseimg";
import QuestionBox from "../questionbox";
function GameScene({theme,id}) {
const [data,setData] = useState([]);
const token = localStorage.getItem('token');
useEffect(() => {
post('https://saucisson.justw.tf/api/games/create/'+id,{},token)
.then(res => {
console.log(res.JSON);
setData(res.JSON);
})
.catch(err => console.log(err));
},[]);
return(
<main className="min-h-screen w-screen pl-44 bg-red-900 flex flex-col justify-center items-center gap-12">
<QuestionBox question={"test"}/>
<img src={img[id]} alt="oponent" className="mt-12 w-48"/>
<AmmoBox game={data}/>
</main>
);
}
export default GameScene;

View File

@@ -0,0 +1,31 @@
import CardMission from "../cardmission";
import { useEffect, useState } from "react";
import { get } from '../../../../modules/fetcher';
function MainMenu() {
const [themes,setThemes] = useState([]);
const token = localStorage.getItem('token');
useEffect(() => {
get('https://saucisson.justw.tf/api/themes',token)
.then(res => {
console.log(res.JSON.themes);
setThemes(res.JSON.themes);
console.log(themes[0]);
})
.catch(err => console.log(err));
},[]);
return(
<main className="min-h-screen w-screen pl-44 bg-mainBlue flex flex-row flex-wrap gap-12 p-8">
{themes.map((theme) => {
return(
<CardMission theme={theme} />
)
})}
</main>
);
}
export default MainMenu;

View File

@@ -0,0 +1,8 @@
import Pose1 from "../../../../assets/perso/perso-1/pose-1.png";
import Pose2 from "../../../../assets/perso/perso-2/pose-1.png";
import Pose3 from "../../../../assets/perso/perso-3/pose-1.png";
import Pose4 from "../../../../assets/perso/perso-4/pose-1.png";
const img = ["",Pose1,Pose2,Pose3,Pose4];
export default img;

View File

@@ -0,0 +1,9 @@
function QuestionBox({question}) {
return(
<div className="fixed top-4 left-32 right-4 h-42 bg-slate-300 rounded-md">
{question}
</div>
);
}
export default QuestionBox;

View File

@@ -0,0 +1,31 @@
function Footer() {
return(
<footer className="bg-darkBlue rounded-lg shadow">
<div className="w-full max-w-screen-xl mx-auto p-4 md:py-8">
<div className="sm:flex sm:items-center sm:justify-between">
<a href="https://flowbite.com/"
className="flex items-center mb-4 sm:mb-0 space-x-3 rtl:space-x-reverse">
<span
className="self-center text-2xl font-semibold whitespace-nowrap text-white">Debate</span>
</a>
<ul className="flex flex-wrap items-center mb-6 text-sm font-medium text-white sm:mb-0">
<li>
<a href="#" className="hover:underline me-4 md:me-6">Home</a>
</li>
<li>
<a href="#" className="hover:underline me-4 md:me-6">Game</a>
</li>
<li>
<a href="#" className="hover:underline me-4 md:me-6">Profile</a>
</li>
</ul>
</div>
<hr className="my-6 border-gray-200 sm:mx-auto dark:border-gray-700 lg:my-8"/>
<span className="block text-sm text-white sm:text-center">© 2023 <a
href="https://flowbite.com/" className="hover:underline">Debate</a>. All Rights Reserved.</span>
</div>
</footer>
);
}
export default Footer;

View File

@@ -0,0 +1,26 @@
// import Pose1 from "../../../../assets/home/baneer.jpg";
import {Link} from "react-router-dom";
function HomeHero() {
return (
<section class="bg-darkBlue md:mt-16 w-full">
<div class="grid max-w-screen-xl px-4 py-8 mx-auto lg:gap-8 xl:gap-0 lg:py-16 lg:grid-cols-12">
<div class="mr-auto place-self-center lg:col-span-7">
<h1 class="max-w-2xl mb-4 text-4xl font-extrabold tracking-tight leading-none md:text-5xl xl:text-6xl text-white">Debate</h1>
<p class="max-w-2xl mb-6 font-light lg:mb-8 md:text-lg lg:text-xl text-gray-400">Platform for immersive climate-themed mystery games, inspired by the thrilling narrative style of Danganronpa.</p>
<Link to='/game' class="inline-flex items-center justify-center px-5 py-3 mr-3 text-base font-medium text-center text-white rounded-lg bg-mainBlue focus:ring-4 dark:focus:ring-blue-900">
Get started
<svg class="w-5 h-5 ml-2 -mr-1" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
</Link>
<a href="#" class="inline-flex items-center justify-center px-5 py-3 text-base font-medium text-center border border-gray-300 rounded-lg hover:bg-white hover:text-black focus:ring-4 focus:ring-gray-100 text-white">
Check my profile
</a>
</div>
<div class="hidden lg:mt-0 lg:col-span-5 lg:flex">
<img src="https://www.cci.fr/sites/g/files/mwbcuj1451/files/2023-03/energie-ecologique.png" alt="mockup" />
</div>
</div>
</section>
);
}
export default HomeHero;

View File

@@ -0,0 +1,18 @@
function SecondSection() {
return (
<section className="bg-white dark:bg-gray-900">
<div className="gap-16 items-center py-8 px-4 mx-auto max-w-screen-xl lg:grid lg:grid-cols-2 lg:py-16 lg:px-6">
<div className="font-light text-gray-500 sm:text-lg dark:text-gray-400">
<h2 className="mb-4 text-4xl tracking-tight font-extrabold text-gray-900 dark:text-white">Let us introduce you to the game</h2>
<p className="mb-4">Welcome to Debate. On this site, you'll find a Dangaronpa-type game. The aim is simple: to find the right answer to refute your opponent's statement. Our different themes and levels on global warming and its consequences will put your knowledge to the test! Good luck</p>
</div>
<div className="grid grid-cols-2 gap-4 mt-8">
<img className="w-full rounded-lg h-96 object-cover" src="https://i0.wp.com/ecolosport.fr/wp-content/uploads/2023/02/matthew-smith-Rfflri94rs8-unsplash-scaled-e1676906506139.jpeg?resize=1440%2C720&ssl=1" alt="office content 1" />
<img className="mt-4 w-full lg:mt-10 rounded-lg h-96 object-cover" src="https://img.freepik.com/photos-premium/environnement-jour-terre-entre-mains-arbres-qui-poussent_34998-113.jpg" alt="office content 2" />
</div>
</div>
</section>
)
}
export default SecondSection

View File

@@ -0,0 +1,8 @@
function Logo() {
return(
<div className="h-12 w-12 bg-red-500">
</div>
)
}
export default Logo;

View File

@@ -0,0 +1,15 @@
import Logo from "../../logo";
import NavListGame from "./navlist";
import list from "../list";
function NavbarreGame() {
return (
<nav className="h-full w-28 fixed top-0 left-0 bg-darkBlue flex flex-col justify-between items-center py-4 text-white">
<Logo />
<NavListGame list={list}/>
<div>by GPO</div>
</nav>
);
}
export default NavbarreGame;

View File

@@ -0,0 +1,20 @@
// import NavLinks from "../navlinks";
// import Link from "react";
function NavListGame({list}) {
list.map((item, index) => {
console.log("Item: " + item + " Index: " + index);
});
return(
<ul className="flex flex-col justify-between items-center gap-3">
{list.map((item, index) => (
<li key={index} className="h-full flex justify-center items-center">
<a href={item[1]} className="text-align">{item[0]}</a>
</li>
))}
</ul>
);
}
export default NavListGame;

View File

@@ -0,0 +1,41 @@
import NavListHome from "./navlist";
import list from "../list";
import LoginButton from "../login-button";
import logout from "../../../lib/logout";
import {useEffect} from "react";
function NavbarreHome() {
return(
<nav className="fixed top-0 left-0 h-16 w-full px-4 flex flex-row justify-between items-center text-center bg-white shadow-2xl">
<img className="h-12 w-12 rounded-full" src="https://media.discordapp.net/attachments/1182365035700428971/1182533295850926090/image_2.png?ex=65850af4&is=657295f4&hm=eca346750009e926b9f0c76c9e6ca4e19c76f3157ad1fc19b8e7624dd8ef97b8&=&format=webp&quality=lossless&width=662&height=662"/>
<NavListHome list={list}/>
{localStorage.getItem('token') !== null ? (
<div>
<button id="dropdownDefaultButton" data-dropdown-toggle="dropdown"
className="text-white bg-darkGreen focus:ring-4 focus:outline-none font-medium rounded-lg text-sm px-5 py-2.5 text-center inline-flex items-center"
type="button">{localStorage.getItem('username')}
<svg className="w-2.5 h-2.5 ms-3" aria-hidden="true"
xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 10 6">
<path stroke="currentColor"
d="m1 1 4 4 4-4"/>
</svg>
</button>
<div id="dropdown"
className="z-10 hidden bg-white divide-y divide-gray-100 rounded-lg shadow w-44">
<ul className="py-2 text-sm text-gray-700"
aria-labelledby="dropdownDefaultButton">
<li>
<a onClick={() => logout()} className="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Logout</a>
</li>
</ul>
</div>
</div>
) : (
<LoginButton/>
)}
</nav>
)
}
export default NavbarreHome;

View File

@@ -0,0 +1,9 @@
// import Link from 'react';
// function NavLinks() {
// return(
// <Link to="test" className="">test</Link>
// );
// }
// export default NavLinks;

View File

@@ -0,0 +1,20 @@
// import NavLinks from "../navlinks";
import Link from "react";
function NavListHome({list}) {
list.map((item, index) => {
console.log("Item: " + item + " Index: " + index);
});
return(
<ul className="flex flex-row justify-between items-center h-full gap-5">
{list.map((item, index) => (
<li key={index} className="h-full flex justify-center items-center">
<a href={item[1]} className="text-align">{item[0]}</a>
</li>
))}
</ul>
);
}
export default NavListHome;

View File

@@ -0,0 +1,7 @@
const list = [
['Home', '/'],
['Game', '/game'],
['Profile', '/profile'],
];
export default list;

View File

@@ -0,0 +1,9 @@
function LoginButton() {
return (
<a href="/login">
<button className="py-3 px-6 bg-darkBlue text-white rounded-md">Login</button>
</a>
)
}
export default LoginButton;

View File

@@ -0,0 +1,15 @@
import NavLinks from "../navlinks";
function NavListHome({list}) {
return(
<ul className="flex flex-row justify-between items-center h-full">
{list.map((item, index) => (
<li key={index} className="h-full">
<NavLinks text={item[0]} link={item[1]} />
</li>
))}
</ul>
);
}
export default NavListHome;

17
webapp/src/index.css Normal file
View File

@@ -0,0 +1,17 @@
body {
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 {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
@tailwind base;
@tailwind components;
@tailwind utilities;

50
webapp/src/index.js Normal file
View File

@@ -0,0 +1,50 @@
import React, {useCallback, useEffect, useState} from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import Home from "./pages/home";
import Login from "./pages/login";
import Game from "./pages/game";
import GameTheme from "./pages/gametheme";
import {BrowserRouter, Navigate, Route, Routes} from "react-router-dom";
import Register from "./pages/register";
import {post} from "./modules/fetcher";
const root = ReactDOM.createRoot(document.getElementById('root'));
function App() {
const [isConnected, setIsConnected] = useState(null)
const checkUserConnection = useCallback(async () => {
try {
const res = await post('https://saucisson.justw.tf/api/users/verify', {'token': localStorage.getItem('token')}, '');
if (res.status === 200) {
setIsConnected(true);
} else {
setIsConnected(false);
}
} catch (error) {
console.error('Erreur lors de la vérification de la connexion:', error);
setIsConnected(false);
}
}, []); // [] en dépendances pour que la fonction soit mémorisée
useEffect(() => {
checkUserConnection();
}, [checkUserConnection, window.location.href]);
return (
<React.StrictMode>
<BrowserRouter>
<Routes>
<Route path="/" element={<Home/>}/>
<Route path="/login" element={isConnected ? <Navigate to="/"/> : <Login />}/>
<Route path="/register" element={<Register />}/>
<Route path="/game" element={<Game />}/>
<Route path="/game/:id" element={isConnected ? <GameTheme /> : <Navigate to="/game" />}/>
</Routes>
</BrowserRouter>
</React.StrictMode>
)
}
root.render(<App />)

7
webapp/src/lib/logout.js Normal file
View File

@@ -0,0 +1,7 @@
function logout() {
localStorage.removeItem('token');
localStorage.removeItem('username');
window.location.reload();
}
export default logout

0
webapp/src/lib/txt Normal file
View File

1
webapp/src/logo.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,69 @@
async function get(url, token) {
const options = {
method: 'GET',
headers: { 'Content-Type': 'application/json', authorization: `${token}` },
};
return await fetch(url, options)
.then(res => res.json())
.then(json => {
return json;
})
}
async function post(url, body, token) {
const options = {
method: 'POST',
mode: 'cors',
headers: { 'Content-Type': 'application/json', authorization: `${token}` },
body: JSON.stringify(body),
};
return await fetch(url, options)
.then(res => res.json())
.then(json => {
return json;
})
}
async function patch(url, body, token) {
const options = {
method: 'PATCH',
mode: 'cors',
headers: { 'Content-Type': 'application/json', authorization: `${token}` },
body: JSON.stringify(body),
};
return await fetch(url, options)
.then(res => res.json())
.then(json => {
return json;
})
}
async function put(url, body, token) {
const options = {
method: 'PUT',
mode: 'cors',
headers: { 'Content-Type': 'application/json', authorization: `${token}` },
body: JSON.stringify(body),
};
return await fetch(url, options)
.then(res => res.json())
.then(json => {
return json;
})
}
export {
get,
post,
patch,
put,
};

View File

@@ -0,0 +1,13 @@
import NavbarreGame from "../../components/navbarre/game";
import MainMenu from "../../components/content/game/mainmenu";
function Game () {
return(
<div className="w-full h-full">
<NavbarreGame />
<MainMenu />
</div>
);
}
export default Game;

View File

@@ -0,0 +1,29 @@
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { get } from "../../modules/fetcher";
import NavbarreGame from "../../components/navbarre/game";
import GameScene from "../../components/content/game/gamescene";
function GameTheme() {
const {id} = useParams();
const [theme,setTheme] = useState([]);
const token = localStorage.getItem('token');
useEffect(() => {
get('https://saucisson.justw.tf/api/themes/'+id, token)
.then(res => {
setTheme(res.JSON.themes);
})
.catch(err => console.log(err));
},[]);
return(
<div className="h-full w-full">
<NavbarreGame />
<GameScene theme={theme} id={id}/>
</div>
);
}
export default GameTheme;

View File

@@ -0,0 +1,17 @@
import NavbarreHome from "../../components/navbarre/home";
import HomeHero from "../../components/content/home/hero";
import Footer from "../../components/content/home/footer";
import SecondSection from "../../components/content/home/secondSection";
function Home() {
return (
<div>
<NavbarreHome/>
<HomeHero />
<SecondSection />
<Footer />
</div>
)
}
export default Home;

View File

@@ -0,0 +1,67 @@
import {Link, redirect, useNavigate} from "react-router-dom";
import {useState} from "react";
import {get, post} from '../../modules/fetcher';
function Login() {
const navigate = useNavigate();
const loginUser = async (e) => {
e.preventDefault()
post('https://saucisson.justw.tf/api/users/login', {username, password}, '')
.then(async res => {
if (res.status === 200) {
localStorage.setItem('username', username)
localStorage.setItem('token', res.JSON.token)
navigate('/')
} else {
setError(res.message)
}
})
}
const [error, setError] = useState(null)
const [username, setUsername] = useState(null)
const [password, setPassword] = useState(null)
return (
<section className="bg-gradient-to-r from-mainBlue to-darkGreen">
<div className="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
<div
className="w-full bg-white rounded-lg shadow md:mt-0 sm:max-w-md xl:p-0">
<div className="p-6 space-y-4 md:space-y-6 sm:p-8">
<h1 className="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl">
Login to your account
</h1>
<form onSubmit={loginUser} className="space-y-4 md:space-y-6">
{error && (
<div>
<p className="text-red-500 text-md">{error}</p>
</div>
)}
<div>
<label htmlFor="email"
className="block mb-2 text-sm font-medium text-gray-900">Your
username</label>
<input type="text" name="email" id="email"
className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-blue-600 focus:border-blue-600 block w-full p-2.5"
placeholder="John Doe" required="" onChange={e => {setUsername(e.target.value)}}/>
</div>
<div>
<label htmlFor="password"
className="block mb-2 text-sm font-medium text-gray-900">Password</label>
<input type="password" name="password" id="password" placeholder="••••••••"
className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-blue-600 focus:border-blue-600 block w-full p-2.5"
required onChange={e => {setPassword(e.target.value)}}/>
</div>
<button className="w-full text-white bg-darkGreen focus:ring-4 focus:outline-none font-medium rounded-lg text-sm px-5 py-2.5 text-center">Login</button>
<p className="text-sm font-light text-gray-500">
Dont have an account yet? <Link to={'/register'}
className="font-medium text-darkGreen hover:underline">Sign
up</Link>
</p>
</form>
</div>
</div>
</div>
</section>
)
}
export default Login

View File

@@ -0,0 +1,81 @@
import {Link, redirect, useNavigate} from "react-router-dom";
import {post} from "../../modules/fetcher";
import {useState} from "react";
function Register() {
const navigate = useNavigate();
const registerUser = async (e) => {
e.preventDefault()
if(password === confirmPassword) {
post('https://saucisson.justw.tf/api/users/register', {username, password}, '')
.then(async res => {
if (res.status === 200) {
localStorage.setItem('username', username)
localStorage.setItem('token', res.JSON.token)
navigate('/')
} else {
setError(res.message)
}
})
} else {
setError("Password doesn't match")
}
}
const [error, setError] = useState(null)
const [username, setUsername] = useState(null)
const [password, setPassword] = useState(null)
const [confirmPassword, setconfirmPassword] = useState(null)
return (
<section className="bg-gradient-to-r from-mainBlue to-darkGreen">
<div className="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
<div
className="w-full bg-white rounded-lg shadow md:mt-0 sm:max-w-md xl:p-0">
<div className="p-6 space-y-4 md:space-y-6 sm:p-8">
<h1 className="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl">
Create an account
</h1>
<form className="space-y-4 md:space-y-6" onSubmit={registerUser}>
{error && (
<div>
<p className="text-red-500 text-md">{error}</p>
</div>
)}
<div>
<label htmlFor="email"
className="block mb-2 text-sm font-medium text-gray-900">Your
username</label>
<input type="text" name="email" id="email" className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-blue-600 focus:border-blue-600 block w-full p-2.5" placeholder="John Doe" required="" onChange={e => {setUsername(e.target.value)}}/>
</div>
<div>
<label htmlFor="password"
className="block mb-2 text-sm font-medium text-gray-900">Password</label>
<input type="password" name="password" id="password" placeholder="••••••••"
className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-blue-600 focus:border-blue-600 block w-full p-2.5"
required="" onChange={e => {setPassword(e.target.value)}}/>
</div>
<div>
<label htmlFor="password"
className="block mb-2 text-sm font-medium text-gray-900">Password confirmation</label>
<input type="password" name="confirm_password" id="confirm_password" placeholder="••••••••"
className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-blue-600 focus:border-blue-600 block w-full p-2.5"
required="" onChange={e => {setconfirmPassword(e.target.value)}}/>
</div>
<button type="submit"
className="w-full text-white bg-darkGreen focus:ring-4 focus:outline-none font-medium rounded-lg text-sm px-5 py-2.5 text-center">Register
</button>
<p className="text-sm font-light text-gray-500">
Dont have an account yet? <Link to={'/login'}
className="font-medium text-darkGreen hover:underline">Sign
up</Link>
</p>
</form>
</div>
</div>
</div>
</section>
)
}
export default Register

0
webapp/src/pages/txt Normal file
View File

26
webapp/tailwind.config.js Normal file
View File

@@ -0,0 +1,26 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
"./node_modules/flowbite/**/*.js"
],
theme: {
extend: {
spacing: {
'dfull' : '-100vw',
'd24': '-24px',
},
colors: {
'darkGreen': '#4dab7f',
'mainBlue': '#047385',
'mainOrange': '#d76d1e',
'darkBlue': '#043743',
}
},
},
plugins: [
require('flowbite/plugin')
],
}