Compare commits
22 Commits
lightemera
...
GringoElPe
| Author | SHA1 | Date | |
|---|---|---|---|
| 98290b41a1 | |||
| 3660e1cd7c | |||
| 8c184944f3 | |||
| 49bc4f64fb | |||
| 36f5e7dfa6 | |||
| 5669bb6be3 | |||
| c49faca15b | |||
| 08a8819d25 | |||
| d2a8189a6b | |||
| 9ae801fea9 | |||
| 9bcded7f9a | |||
| 80f786ff61 | |||
| ddfdcd613f | |||
| 95eec4f93e | |||
| 590b6773f8 | |||
| bca2ca59e6 | |||
| 9b6403f9a5 | |||
| a441c3e78b | |||
| 6a91e0739a | |||
| a9ed1691f2 | |||
| c4ce652ab3 | |||
| 5a5600c375 |
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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/
|
||||
@@ -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 };
|
||||
@@ -1,13 +0,0 @@
|
||||
# nuitdelinfo2023-api
|
||||
|
||||
To install dependencies:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
To run:
|
||||
|
||||
```bash
|
||||
bun run index.js
|
||||
```
|
||||
BIN
api/bun.lockb
148
api/database.sql
@@ -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 n’assaini 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, 'L’air 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 à l’horizon 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 c’est l’activité économique qui pollue.'),
|
||||
(43, 4, 'La France est une goutte d’eau : ce sont la Chine et l’Inde 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 d’ici 2100 ?'),
|
||||
(58, 2, 'Efficacite des energies renouvelables'),
|
||||
(59, 5, 'Le réchauffement climatique, c’est 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, 'L’augmentation de la température moyenne de la planète est due à un cycle naturel et n’a 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é n’est pas menacée, il y a toujours autant d’espè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 l’environnement 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 l’environnement 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 n’affecte 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, 'L’agriculture biologique est plus respectueuse de l’environnement et de la santé que l’agriculture conventionnelle.'),
|
||||
(77, 5, 'politiques environnementales strictes'),
|
||||
(78, 1, 'Les végétariens et les végétaliens sont plus respectueux de l’environnement et de la santé que les omnivores.');
|
||||
39
api/index.js
@@ -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}`);
|
||||
});
|
||||
@@ -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
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
@@ -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
@@ -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
43
webapp/package.json
Normal 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
|
After Width: | Height: | Size: 3.8 KiB |
44
webapp/public/index.html
Normal 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
|
After Width: | Height: | Size: 5.2 KiB |
BIN
webapp/public/logo512.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
25
webapp/public/manifest.json
Normal 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
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
BIN
webapp/src/assets/perso/perso-1/pose-1.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
webapp/src/assets/perso/perso-1/pose-2.png
Normal file
|
After Width: | Height: | Size: 156 KiB |
BIN
webapp/src/assets/perso/perso-2/pose-1.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
webapp/src/assets/perso/perso-3/pose-1.png
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
webapp/src/assets/perso/perso-4/pose-1.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
webapp/src/assets/perso/perso-4/pose-2.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
12
webapp/src/components/content/game/ammo/index.jsx
Normal 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;
|
||||
16
webapp/src/components/content/game/ammobox/index.jsx
Normal 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;
|
||||
50
webapp/src/components/content/game/cardmission/index.jsx
Normal 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;
|
||||
30
webapp/src/components/content/game/gamescene/index.jsx
Normal 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;
|
||||
31
webapp/src/components/content/game/mainmenu/index.jsx
Normal 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;
|
||||
8
webapp/src/components/content/game/poseimg/index.jsx
Normal 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;
|
||||
9
webapp/src/components/content/game/questionbox/index.jsx
Normal 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;
|
||||
31
webapp/src/components/content/home/footer/index.jsx
Normal 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;
|
||||
26
webapp/src/components/content/home/hero/index.jsx
Normal 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;
|
||||
18
webapp/src/components/content/home/secondSection/index.jsx
Normal 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
|
||||
8
webapp/src/components/logo/index.jsx
Normal file
@@ -0,0 +1,8 @@
|
||||
function Logo() {
|
||||
return(
|
||||
<div className="h-12 w-12 bg-red-500">
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Logo;
|
||||
15
webapp/src/components/navbarre/game/index.jsx
Normal 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;
|
||||
20
webapp/src/components/navbarre/game/navlist/index.jsx
Normal 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;
|
||||
41
webapp/src/components/navbarre/home/index.jsx
Normal 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;
|
||||
9
webapp/src/components/navbarre/home/navlinks/index.jsx
Normal file
@@ -0,0 +1,9 @@
|
||||
// import Link from 'react';
|
||||
|
||||
// function NavLinks() {
|
||||
// return(
|
||||
// <Link to="test" className="">test</Link>
|
||||
// );
|
||||
// }
|
||||
|
||||
// export default NavLinks;
|
||||
20
webapp/src/components/navbarre/home/navlist/index.jsx
Normal 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;
|
||||
7
webapp/src/components/navbarre/list/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const list = [
|
||||
['Home', '/'],
|
||||
['Game', '/game'],
|
||||
['Profile', '/profile'],
|
||||
];
|
||||
|
||||
export default list;
|
||||
9
webapp/src/components/navbarre/login-button/index.jsx
Normal 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;
|
||||
15
webapp/src/components/navbarre/navlist/index.jsx
Normal 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
@@ -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
@@ -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
@@ -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
1
webapp/src/logo.svg
Normal 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 |
69
webapp/src/modules/fetcher.js
Normal 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,
|
||||
};
|
||||
13
webapp/src/pages/game/index.jsx
Normal 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;
|
||||
29
webapp/src/pages/gametheme/index.jsx
Normal 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;
|
||||
17
webapp/src/pages/home/index.jsx
Normal 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;
|
||||
67
webapp/src/pages/login/index.jsx
Normal 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">
|
||||
Don’t 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
|
||||
81
webapp/src/pages/register/index.jsx
Normal 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">
|
||||
Don’t 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
26
webapp/tailwind.config.js
Normal 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')
|
||||
],
|
||||
}
|
||||
|
||||