diff --git a/.env b/.env
index 131b17e..cc721dd 100644
--- a/.env
+++ b/.env
@@ -1 +1 @@
-teamname="wiq_0"
\ No newline at end of file
+teamname="wiq_7"
\ No newline at end of file
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 48a92d3..83bd981 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -8,37 +8,37 @@ jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-node@v4
- with:
- node-version: 20
- - run: npm --prefix users/authservice ci
- - run: npm --prefix users/userservice ci
- - run: npm --prefix gatewayservice ci
- - run: npm --prefix webapp ci
- - run: npm --prefix users/authservice test -- --coverage
- - run: npm --prefix users/userservice test -- --coverage
- - run: npm --prefix gatewayservice test -- --coverage
- - run: npm --prefix webapp test -- --coverage
- - name: Analyze with SonarCloud
- uses: sonarsource/sonarcloud-github-action@master
- env:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ - run: npm --prefix users/authservice ci
+ - run: npm --prefix users/userservice ci
+ - run: npm --prefix gatewayservice ci
+ - run: npm --prefix webapp ci
+ - run: npm --prefix users/authservice test -- --coverage
+ - run: npm --prefix users/userservice test -- --coverage
+ - run: npm --prefix gatewayservice test -- --coverage
+ - run: npm --prefix webapp test -- --coverage
+ - name: Analyze with SonarCloud
+ uses: sonarsource/sonarcloud-github-action@master
+ env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
e2e-tests:
needs: [unit-tests]
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-node@v4
- with:
- node-version: 20
- - run: npm --prefix users/authservice install
- - run: npm --prefix users/userservice install
- - run: npm --prefix gatewayservice install
- - run: npm --prefix webapp install
- - run: npm --prefix webapp run build
- - run: npm --prefix webapp run test:e2e
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ - run: npm --prefix users/authservice install
+ - run: npm --prefix users/userservice install
+ - run: npm --prefix gatewayservice install
+ - run: npm --prefix webapp install
+ - run: npm --prefix webapp run build
+ - run: npm --prefix webapp run test:e2e
docker-push-webapp:
name: Push webapp Docker Image to GitHub Packages
runs-on: ubuntu-latest
@@ -47,13 +47,13 @@ jobs:
packages: write
needs: [e2e-tests]
steps:
- - uses: actions/checkout@v4
- - name: Publish to Registry
- uses: elgohr/Publish-Docker-Github-Action@v5
- env:
- API_URI: http://${{ secrets.DEPLOY_HOST }}:8000
- with:
- name: arquisoft/wiq_0/webapp
+ - uses: actions/checkout@v4
+ - name: Publish to Registry
+ uses: elgohr/Publish-Docker-Github-Action@v5
+ env:
+ API_URI: http://${{ secrets.DEPLOY_HOST }}:8000
+ with:
+ name: arquisoft/wiq_7/webapp
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
registry: ghcr.io
@@ -67,11 +67,11 @@ jobs:
packages: write
needs: [e2e-tests]
steps:
- - uses: actions/checkout@v4
- - name: Publish to Registry
- uses: elgohr/Publish-Docker-Github-Action@v5
- with:
- name: arquisoft/wiq_0/authservice
+ - uses: actions/checkout@v4
+ - name: Publish to Registry
+ uses: elgohr/Publish-Docker-Github-Action@v5
+ with:
+ name: arquisoft/wiq_7/authservice
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
registry: ghcr.io
@@ -84,15 +84,15 @@ jobs:
packages: write
needs: [e2e-tests]
steps:
- - uses: actions/checkout@v4
- - name: Publish to Registry
- uses: elgohr/Publish-Docker-Github-Action@v5
- with:
- name: arquisoft/wiq_0/userservice
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
- registry: ghcr.io
- workdir: users/userservice
+ - uses: actions/checkout@v4
+ - name: Publish to Registry
+ uses: elgohr/Publish-Docker-Github-Action@v5
+ with:
+ name: arquisoft/wiq_7/userservice
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ registry: ghcr.io
+ workdir: users/userservice
docker-push-gatewayservice:
name: Push gateway service Docker Image to GitHub Packages
runs-on: ubuntu-latest
@@ -101,32 +101,38 @@ jobs:
packages: write
needs: [e2e-tests]
steps:
- - uses: actions/checkout@v4
- - name: Update OpenAPI configuration
- run: |
- DEPLOY_HOST=${{ secrets.DEPLOY_HOST }}
- sed -i "s/SOMEIP/${DEPLOY_HOST}/g" gatewayservice/openapi.yaml
- - name: Publish to Registry
- uses: elgohr/Publish-Docker-Github-Action@v5
- with:
- name: arquisoft/wiq_0/gatewayservice
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
- registry: ghcr.io
- workdir: gatewayservice
+ - uses: actions/checkout@v4
+ - name: Update OpenAPI configuration
+ run: |
+ DEPLOY_HOST=${{ secrets.DEPLOY_HOST }}
+ sed -i "s/SOMEIP/${DEPLOY_HOST}/g" gatewayservice/openapi.yaml
+ - name: Publish to Registry
+ uses: elgohr/Publish-Docker-Github-Action@v5
+ with:
+ name: arquisoft/wiq_7/gatewayservice
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ registry: ghcr.io
+ workdir: gatewayservice
deploy:
name: Deploy over SSH
runs-on: ubuntu-latest
- needs: [docker-push-userservice,docker-push-authservice,docker-push-gatewayservice,docker-push-webapp]
+ needs:
+ [
+ docker-push-userservice,
+ docker-push-authservice,
+ docker-push-gatewayservice,
+ docker-push-webapp,
+ ]
steps:
- - name: Deploy over SSH
- uses: fifsky/ssh-action@master
- with:
- host: ${{ secrets.DEPLOY_HOST }}
- user: ${{ secrets.DEPLOY_USER }}
- key: ${{ secrets.DEPLOY_KEY }}
- command: |
- wget https://raw.githubusercontent.com/arquisoft/wiq_0/master/docker-compose.yml -O docker-compose.yml
- wget https://raw.githubusercontent.com/arquisoft/wiq_0/master/.env -O .env
- docker compose --profile prod down
- docker compose --profile prod up -d --pull always
+ - name: Deploy over SSH
+ uses: fifsky/ssh-action@master
+ with:
+ host: ${{ secrets.DEPLOY_HOST }}
+ user: ${{ secrets.DEPLOY_USER }}
+ key: ${{ secrets.DEPLOY_KEY }}
+ command: |
+ wget https://raw.githubusercontent.com/arquisoft/wiq_7/master/docker-compose.yml -O docker-compose.yml
+ wget https://raw.githubusercontent.com/arquisoft/wiq_7/master/.env -O .env
+ docker compose --profile prod down
+ docker compose --profile prod up -d --pull always
diff --git a/README.md b/README.md
index e78fdd6..fc181e0 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,10 @@
-# wiq_0
+# wiq_7
[![Deploy on release](https://github.com/Arquisoft/wiq_0/actions/workflows/release.yml/badge.svg)](https://github.com/Arquisoft/wiq_0/actions/workflows/release.yml)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=Arquisoft_wiq_0&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=Arquisoft_wiq_0)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=Arquisoft_wiq_0&metric=coverage)](https://sonarcloud.io/summary/new_code?id=Arquisoft_wiq_0)
-This is a base repo for the [Software Architecture course](http://arquisoft.github.io/) in [2023/2024 edition](https://arquisoft.github.io/course2324.html).
+This is a base repo for the [Software Architecture course](http://arquisoft.github.io/) in [2023/2024 edition](https://arquisoft.github.io/course2324.html).
This repo is a basic application composed of several components.
@@ -41,7 +41,7 @@ docker compose --profile dev down
First, start the database. Either install and run Mongo or run it using docker:
-```docker run -d -p 27017:27017 --name=my-mongo mongo:latest```
+`docker run -d -p 27017:27017 --name=my-mongo mongo:latest`
You can also use services like Mongo Altas for running a Mongo database in the cloud.
@@ -53,11 +53,11 @@ After all the components are launched, the app should be available in localhost
## Deployment
-For the deployment, we have several options.
+For the deployment, we have several options.
-The first and more flexible is to deploy to a virtual machine using SSH. This will work with any cloud service (or with our own server).
+The first and more flexible is to deploy to a virtual machine using SSH. This will work with any cloud service (or with our own server).
-Other options include using the container services that most cloud services provide. This means, deploying our Docker containers directly.
+Other options include using the container services that most cloud services provide. This means, deploying our Docker containers directly.
We are going to use the first approach, creating a virtual machine in a cloud service and after installing docker and docker-compose, deploy our containers there using GitHub Actions and SSH.
@@ -85,9 +85,9 @@ sudo chmod +x /usr/local/bin/docker-compose
### Continuous delivery (GitHub Actions)
-Once we have our machine ready, we could deploy by hand the application, taking our docker-compose file and executing it in the remote machine.
+Once we have our machine ready, we could deploy by hand the application, taking our docker-compose file and executing it in the remote machine.
-In this repository, this process is done automatically using **GitHub Actions**. The idea is to trigger a series of actions when some condition is met in the repository.
+In this repository, this process is done automatically using **GitHub Actions**. The idea is to trigger a series of actions when some condition is met in the repository.
As you can see, unitary tests of each module and e2e tests are executed before pushing the docker images and deploying them. Using this approach we avoid deploying versions that do not pass the tests.
@@ -95,10 +95,16 @@ The deploy action is the following:
```yml
deploy:
- name: Deploy over SSH
- runs-on: ubuntu-latest
- needs: [docker-push-userservice,docker-push-authservice,docker-push-gatewayservice,docker-push-webapp]
- steps:
+ name: Deploy over SSH
+ runs-on: ubuntu-latest
+ needs:
+ [
+ docker-push-userservice,
+ docker-push-authservice,
+ docker-push-gatewayservice,
+ docker-push-webapp,
+ ]
+ steps:
- name: Deploy over SSH
uses: fifsky/ssh-action@master
with:
@@ -113,9 +119,10 @@ deploy:
```
This action uses three secrets that must be configured in the repository:
+
- DEPLOY_HOST: IP of the remote machine.
- DEPLOY_USER: user with permission to execute the commands in the remote machine.
- DEPLOY_KEY: key to authenticate the user in the remote machine.
-Note that this action logs in the remote machine and downloads the docker-compose file from the repository and launches it.
+Note that this action logs in the remote machine and downloads the docker-compose file from the repository and launches it.
Obviously, previous actions have been executed which have uploaded the docker images to the GitHub Packages repository.
diff --git a/docker-compose.yml b/docker-compose.yml
index c105ed5..c0fdf57 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -3,23 +3,23 @@ services:
mongodb:
container_name: mongodb-${teamname:-defaultASW}
image: mongo
- profiles: ["dev", "prod"]
+ profiles: ['dev', 'prod']
volumes:
- mongodb_data:/data/db
ports:
- - "27017:27017"
+ - '27017:27017'
networks:
- mynetwork
authservice:
container_name: authservice-${teamname:-defaultASW}
- image: ghcr.io/arquisoft/wiq_0/authservice:latest
- profiles: ["dev", "prod"]
+ image: ghcr.io/arquisoft/wiq_7/authservice:latest
+ profiles: ['dev', 'prod']
build: ./users/authservice
depends_on:
- mongodb
ports:
- - "8002:8002"
+ - '8002:8002'
networks:
- mynetwork
environment:
@@ -27,13 +27,13 @@ services:
userservice:
container_name: userservice-${teamname:-defaultASW}
- image: ghcr.io/arquisoft/wiq_0/userservice:latest
- profiles: ["dev", "prod"]
+ image: ghcr.io/arquisoft/wiq_7/userservice:latest
+ profiles: ['dev', 'prod']
build: ./users/userservice
depends_on:
- mongodb
ports:
- - "8001:8001"
+ - '8001:8001'
networks:
- mynetwork
environment:
@@ -41,15 +41,15 @@ services:
gatewayservice:
container_name: gatewayservice-${teamname:-defaultASW}
- image: ghcr.io/arquisoft/wiq_0/gatewayservice:latest
- profiles: ["dev", "prod"]
+ image: ghcr.io/arquisoft/wiq_7/gatewayservice:latest
+ profiles: ['dev', 'prod']
build: ./gatewayservice
depends_on:
- mongodb
- userservice
- authservice
ports:
- - "8000:8000"
+ - '8000:8000'
networks:
- mynetwork
environment:
@@ -58,32 +58,32 @@ services:
webapp:
container_name: webapp-${teamname:-defaultASW}
- image: ghcr.io/arquisoft/wiq_0/webapp:latest
- profiles: ["dev", "prod"]
+ image: ghcr.io/arquisoft/wiq_7/webapp:latest
+ profiles: ['dev', 'prod']
build: ./webapp
depends_on:
- gatewayservice
ports:
- - "3000:3000"
+ - '3000:3000'
prometheus:
image: prom/prometheus
container_name: prometheus-${teamname:-defaultASW}
- profiles: ["dev"]
+ profiles: ['dev']
networks:
- mynetwork
volumes:
- ./gatewayservice/monitoring/prometheus:/etc/prometheus
- prometheus_data:/prometheus
ports:
- - "9090:9090"
- depends_on:
+ - '9090:9090'
+ depends_on:
- gatewayservice
-
+
grafana:
image: grafana/grafana
container_name: grafana-${teamname:-defaultASW}
- profiles: ["dev"]
+ profiles: ['dev']
networks:
- mynetwork
volumes:
@@ -95,15 +95,14 @@ services:
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
ports:
- - "9091:9091"
- depends_on:
+ - '9091:9091'
+ depends_on:
- prometheus
-
volumes:
mongodb_data:
- prometheus_data:
- grafana_data:
+ prometheus_data:
+ grafana_data:
networks:
mynetwork:
diff --git a/gatewayservice/package.json b/gatewayservice/package.json
index 1d5f9df..b70599e 100644
--- a/gatewayservice/package.json
+++ b/gatewayservice/package.json
@@ -1,33 +1,33 @@
{
- "name": "gatewayservice",
- "version": "1.0.0",
- "description": "Gateway service, in charge distributing the requests",
- "main": "service.js",
- "scripts": {
- "start": "node gateway-service.js",
- "test": "jest"
- },
- "repository": {
- "type": "git",
- "url": "git+https://github.com/arquisoft/wiq_0.git"
- },
- "author": "",
- "license": "ISC",
- "bugs": {
- "url": "https://github.com/arquisoft/wiq_0/issues"
- },
- "homepage": "https://github.com/arquisoft/wiq_0#readme",
- "dependencies": {
- "axios": "^1.6.5",
- "cors": "^2.8.5",
- "express": "^4.18.2",
- "express-openapi": "^12.1.3",
- "express-prom-bundle": "^7.0.0",
- "swagger-ui-express": "^5.0.0",
- "yaml": "^2.4.1"
- },
- "devDependencies": {
- "jest": "^29.7.0",
- "supertest": "^6.3.4"
- }
+ "name": "gatewayservice",
+ "version": "1.0.0",
+ "description": "Gateway service, in charge distributing the requests",
+ "main": "service.js",
+ "scripts": {
+ "start": "node gateway-service.js",
+ "test": "jest"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/arquisoft/wiq_7.git"
+ },
+ "author": "",
+ "license": "ISC",
+ "bugs": {
+ "url": "https://github.com/arquisoft/wiq_7/issues"
+ },
+ "homepage": "https://github.com/arquisoft/wiq_7#readme",
+ "dependencies": {
+ "axios": "^1.6.5",
+ "cors": "^2.8.5",
+ "express": "^4.18.2",
+ "express-openapi": "^12.1.3",
+ "express-prom-bundle": "^7.0.0",
+ "swagger-ui-express": "^5.0.0",
+ "yaml": "^2.4.1"
+ },
+ "devDependencies": {
+ "jest": "^29.7.0",
+ "supertest": "^6.3.4"
+ }
}
diff --git a/sonar-project.properties b/sonar-project.properties
index 8ce93a6..def6844 100644
--- a/sonar-project.properties
+++ b/sonar-project.properties
@@ -1,14 +1,14 @@
-sonar.projectKey=Arquisoft_wiq_es6b
+sonar.projectKey=Arquisoft_wiq_7
sonar.organization=arquisoft
# This is the name and version displayed in the SonarCloud UI.
-sonar.projectName=wiq_es6b
+sonar.projectName=wiq_7
sonar.projectVersion=1.0
# Encoding of the source code. Default is default system encoding
sonar.host.url=https://sonarcloud.io
sonar.language=js
-sonar.projectName=wiq_es6b
+sonar.projectName=wiq_7
sonar.coverage.exclusions=**/*.test.js
sonar.sources=webapp/src/components,users/authservice,users/userservice,gatewayservice
diff --git a/users/authservice/package.json b/users/authservice/package.json
index 6b5b623..81622be 100644
--- a/users/authservice/package.json
+++ b/users/authservice/package.json
@@ -1,32 +1,32 @@
{
- "name": "authservice",
- "version": "1.0.0",
- "description": " Authentication service, in charge of authenticating users in the application",
- "main": "service.js",
- "scripts": {
- "start": "node auth-service.js",
- "test": "jest"
- },
- "repository": {
- "type": "git",
- "url": "git+https://github.com/arquisoft/wiq_0.git"
- },
- "author": "",
- "license": "ISC",
- "bugs": {
- "url": "https://github.com/arquisoft/wiq_0/issues"
- },
- "homepage": "https://github.com/arquisoft/wiq_0#readme",
- "dependencies": {
- "bcrypt": "^5.1.1",
- "body-parser": "^1.20.2",
- "express": "^4.18.2",
- "jsonwebtoken": "^9.0.2",
- "mongoose": "^8.0.4"
- },
- "devDependencies": {
- "jest": "^29.7.0",
- "mongodb-memory-server": "^9.1.5",
- "supertest": "^6.3.4"
- }
+ "name": "authservice",
+ "version": "1.0.0",
+ "description": " Authentication service, in charge of authenticating users in the application",
+ "main": "service.js",
+ "scripts": {
+ "start": "node auth-service.js",
+ "test": "jest"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/arquisoft/wiq_7.git"
+ },
+ "author": "",
+ "license": "ISC",
+ "bugs": {
+ "url": "https://github.com/arquisoft/wiq_7/issues"
+ },
+ "homepage": "https://github.com/arquisoft/wiq_7#readme",
+ "dependencies": {
+ "bcrypt": "^5.1.1",
+ "body-parser": "^1.20.2",
+ "express": "^4.18.2",
+ "jsonwebtoken": "^9.0.2",
+ "mongoose": "^8.0.4"
+ },
+ "devDependencies": {
+ "jest": "^29.7.0",
+ "mongodb-memory-server": "^9.1.5",
+ "supertest": "^6.3.4"
+ }
}
diff --git a/users/userservice/package.json b/users/userservice/package.json
index 2462c8e..e68ce33 100644
--- a/users/userservice/package.json
+++ b/users/userservice/package.json
@@ -9,14 +9,14 @@
},
"repository": {
"type": "git",
- "url": "git+https://github.com/arquisoft/wiq_0.git"
+ "url": "git+https://github.com/arquisoft/wiq_7.git"
},
"author": "",
"license": "ISC",
"bugs": {
- "url": "https://github.com/arquisoft/wiq_0/issues"
+ "url": "https://github.com/arquisoft/wiq_7/issues"
},
- "homepage": "https://github.com/arquisoft/wiq_0#readme",
+ "homepage": "https://github.com/arquisoft/wiq_7#readme",
"dependencies": {
"bcrypt": "^5.1.1",
"body-parser": "^1.20.2",
diff --git a/users/userservice/user-model.js b/users/userservice/user-model.js
index 71d81b5..7540e29 100644
--- a/users/userservice/user-model.js
+++ b/users/userservice/user-model.js
@@ -1,20 +1,28 @@
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
- username: {
- type: String,
- required: true,
- },
- password: {
- type: String,
- required: true,
- },
- createdAt: {
- type: Date,
- default: Date.now,
- },
+ name: String,
+ lastName: String,
+ email: String,
+ username: {
+ type: String,
+ required: true,
+ },
+ password: {
+ type: String,
+ required: true,
+ },
+ role: {
+ type: String,
+ enum: ['admin', 'user'],
+ default: 'user',
+ },
+ createdAt: {
+ type: Date,
+ default: Date.now,
+ },
});
const User = mongoose.model('User', userSchema);
-module.exports = User
\ No newline at end of file
+module.exports = User;
diff --git a/users/userservice/user-service.js b/users/userservice/user-service.js
index be95842..decff39 100644
--- a/users/userservice/user-service.js
+++ b/users/userservice/user-service.js
@@ -3,7 +3,9 @@ const express = require('express');
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const bodyParser = require('body-parser');
-const User = require('./user-model')
+const User = require('./user-model');
+const { get } = require('http');
+const { emit } = require('process');
const app = express();
const port = 8001;
@@ -15,35 +17,49 @@ app.use(bodyParser.json());
const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/userdb';
mongoose.connect(mongoUri);
-
-
// Function to validate required fields in the request body
function validateRequiredFields(req, requiredFields) {
- for (const field of requiredFields) {
- if (!(field in req.body)) {
- throw new Error(`Missing required field: ${field}`);
- }
+ for (const field of requiredFields) {
+ if (!(field in req.body)) {
+ throw new Error(`Missing required field: ${field}`);
}
+ }
}
+// add a new user
app.post('/adduser', async (req, res) => {
- try {
- // Check if required fields are present in the request body
- validateRequiredFields(req, ['username', 'password']);
+ try {
+ // Check if required fields are present in the request body
+ validateRequiredFields(req, ['username', 'password']);
+
+ // Encrypt the password before saving it
+ const hashedPassword = await bcrypt.hash(req.body.password, 10);
- // Encrypt the password before saving it
- const hashedPassword = await bcrypt.hash(req.body.password, 10);
+ const newUser = new User({
+ name: req.body.name,
+ lastName: req.body.lastName,
+ email: req.body.email,
+ username: req.body.username,
+ password: hashedPassword,
+ role: req.body.role,
+ });
- const newUser = new User({
- username: req.body.username,
- password: hashedPassword,
- });
+ await newUser.save();
+ res.json(newUser);
+ } catch (error) {
+ res.status(400).json({ error: error.message });
+ }
+});
- await newUser.save();
- res.json(newUser);
- } catch (error) {
- res.status(400).json({ error: error.message });
- }});
+// get users
+app.get('/users', async (req, res) => {
+ try {
+ const users = await User.find(); // Fetch all users, only return username field for security
+ res.json(users);
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+});
const server = app.listen(port, () => {
console.log(`User Service listening at http://localhost:${port}`);
@@ -51,8 +67,8 @@ const server = app.listen(port, () => {
// Listen for the 'close' event on the Express.js server
server.on('close', () => {
- // Close the Mongoose connection
- mongoose.connection.close();
- });
+ // Close the Mongoose connection
+ mongoose.connection.close();
+});
-module.exports = server
\ No newline at end of file
+module.exports = server;
diff --git a/webapp/package-lock.json b/webapp/package-lock.json
index bcc358d..59c2e5a 100644
--- a/webapp/package-lock.json
+++ b/webapp/package-lock.json
@@ -17,7 +17,10 @@
"axios": "^1.6.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-icons": "^5.3.0",
+ "react-router-dom": "^6.26.2",
"react-scripts": "5.0.1",
+ "styled-components": "^6.1.13",
"web-vitals": "^3.5.1"
},
"devDependencies": {
@@ -2396,9 +2399,9 @@
"integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ=="
},
"node_modules/@emotion/is-prop-valid": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz",
- "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==",
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz",
+ "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==",
"dependencies": {
"@emotion/memoize": "^0.8.1"
}
@@ -5026,6 +5029,14 @@
"node": ">=12"
}
},
+ "node_modules/@remix-run/router": {
+ "version": "1.19.2",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.2.tgz",
+ "integrity": "sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA==",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/@rollup/plugin-babel": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@@ -5982,6 +5993,11 @@
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="
},
+ "node_modules/@types/stylis": {
+ "version": "4.2.5",
+ "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz",
+ "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw=="
+ },
"node_modules/@types/testing-library__jest-dom": {
"version": "5.14.9",
"resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.9.tgz",
@@ -7827,6 +7843,14 @@
"node": ">= 6"
}
},
+ "node_modules/camelize": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
+ "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/caniuse-api": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
@@ -8680,6 +8704,14 @@
"postcss": "^8.4"
}
},
+ "node_modules/css-color-keywords": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
+ "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/css-declaration-sorter": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz",
@@ -8861,6 +8893,16 @@
"resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz",
"integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w=="
},
+ "node_modules/css-to-react-native": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
+ "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
+ "dependencies": {
+ "camelize": "^1.0.0",
+ "css-color-keywords": "^1.0.0",
+ "postcss-value-parser": "^4.0.2"
+ }
+ },
"node_modules/css-tree": {
"version": "1.0.0-alpha.37",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz",
@@ -20174,9 +20216,9 @@
}
},
"node_modules/postcss": {
- "version": "8.4.33",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz",
- "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==",
+ "version": "8.4.38",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
+ "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
"funding": [
{
"type": "opencollective",
@@ -20194,7 +20236,7 @@
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.0",
- "source-map-js": "^1.0.2"
+ "source-map-js": "^1.2.0"
},
"engines": {
"node": "^10 || ^12 || >=14"
@@ -22017,6 +22059,14 @@
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz",
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg=="
},
+ "node_modules/react-icons": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.3.0.tgz",
+ "integrity": "sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==",
+ "peerDependencies": {
+ "react": "*"
+ }
+ },
"node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
@@ -22030,6 +22080,36 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-router": {
+ "version": "6.26.2",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.2.tgz",
+ "integrity": "sha512-tvN1iuT03kHgOFnLPfLJ8V95eijteveqdOSk+srqfePtQvqCExB8eHOYnlilbOcyJyKnYkr1vJvf7YqotAJu1A==",
+ "dependencies": {
+ "@remix-run/router": "1.19.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.26.2",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.2.tgz",
+ "integrity": "sha512-z7YkaEW0Dy35T3/QKPYB1LjMK2R1fxnHO8kWpUMTBdfVzZrWOiY9a7CtN8HqdWtDUWd5FY6Dl8HFsqVwH4uOtQ==",
+ "dependencies": {
+ "@remix-run/router": "1.19.2",
+ "react-router": "6.26.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
"node_modules/react-scripts": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
@@ -24601,6 +24681,11 @@
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
},
+ "node_modules/shallowequal": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
+ "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -24884,9 +24969,9 @@
}
},
"node_modules/source-map-js": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
- "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"engines": {
"node": ">=0.10.0"
}
@@ -25506,6 +25591,38 @@
"webpack": "^5.0.0"
}
},
+ "node_modules/styled-components": {
+ "version": "6.1.13",
+ "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.13.tgz",
+ "integrity": "sha512-M0+N2xSnAtwcVAQeFEsGWFFxXDftHUD7XrKla06QbpUMmbmtFBMMTcKWvFXtWxuD5qQkB8iU5gk6QASlx2ZRMw==",
+ "dependencies": {
+ "@emotion/is-prop-valid": "1.2.2",
+ "@emotion/unitless": "0.8.1",
+ "@types/stylis": "4.2.5",
+ "css-to-react-native": "3.2.0",
+ "csstype": "3.1.3",
+ "postcss": "8.4.38",
+ "shallowequal": "1.1.0",
+ "stylis": "4.3.2",
+ "tslib": "2.6.2"
+ },
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/styled-components"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8.0",
+ "react-dom": ">= 16.8.0"
+ }
+ },
+ "node_modules/styled-components/node_modules/stylis": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz",
+ "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg=="
+ },
"node_modules/stylehacks": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz",
diff --git a/webapp/package.json b/webapp/package.json
index 6e59b09..2e30d72 100644
--- a/webapp/package.json
+++ b/webapp/package.json
@@ -12,7 +12,10 @@
"axios": "^1.6.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-icons": "^5.3.0",
+ "react-router-dom": "^6.26.2",
"react-scripts": "5.0.1",
+ "styled-components": "^6.1.13",
"web-vitals": "^3.5.1"
},
"scripts": {
diff --git a/webapp/public/favicon.ico b/webapp/public/favicon.ico
index a11777c..337afef 100644
Binary files a/webapp/public/favicon.ico and b/webapp/public/favicon.ico differ
diff --git a/webapp/public/index.html b/webapp/public/index.html
index aa069f2..11e2814 100644
--- a/webapp/public/index.html
+++ b/webapp/public/index.html
@@ -24,7 +24,7 @@
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`.
-->
-
React App
+ WIQ 7
You need to enable JavaScript to run this app.
diff --git a/webapp/src/App.css b/webapp/src/App.css
deleted file mode 100644
index 74b5e05..0000000
--- a/webapp/src/App.css
+++ /dev/null
@@ -1,38 +0,0 @@
-.App {
- text-align: center;
-}
-
-.App-logo {
- height: 40vmin;
- pointer-events: none;
-}
-
-@media (prefers-reduced-motion: no-preference) {
- .App-logo {
- animation: App-logo-spin infinite 20s linear;
- }
-}
-
-.App-header {
- background-color: #282c34;
- min-height: 100vh;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- font-size: calc(10px + 2vmin);
- color: white;
-}
-
-.App-link {
- color: #61dafb;
-}
-
-@keyframes App-logo-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
-}
diff --git a/webapp/src/App.js b/webapp/src/App.js
deleted file mode 100644
index 910935a..0000000
--- a/webapp/src/App.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import React, { useState } from 'react';
-import AddUser from './components/AddUser';
-import Login from './components/Login';
-import CssBaseline from '@mui/material/CssBaseline';
-import Container from '@mui/material/Container';
-import Typography from '@mui/material/Typography';
-import Link from '@mui/material/Link';
-
-function App() {
- const [showLogin, setShowLogin] = useState(true);
-
- const handleToggleView = () => {
- setShowLogin(!showLogin);
- };
-
- return (
-
-
-
- Welcome to wiq_0
-
- {showLogin ? : }
-
- {showLogin ? (
-
- Don't have an account? Register here.
-
- ) : (
-
- Already have an account? Login here.
-
- )}
-
-
- );
-}
-
-export default App;
diff --git a/webapp/src/App.jsx b/webapp/src/App.jsx
new file mode 100644
index 0000000..a7798b2
--- /dev/null
+++ b/webapp/src/App.jsx
@@ -0,0 +1,75 @@
+import { createBrowserRouter, RouterProvider } from 'react-router-dom';
+import {
+ HomeLayout,
+ Landing,
+ AddUser,
+ Login,
+ DashboardLayout,
+ Error,
+ Play,
+ Ranking,
+ Stats,
+ Profile,
+ Admin,
+} from './pages';
+
+export const checkDefaultTheme = () => {
+ const isDarkTheme = localStorage.getItem('darkTheme') === 'true';
+ document.body.classList.toggle('dark-theme', isDarkTheme);
+ return isDarkTheme;
+};
+
+checkDefaultTheme();
+
+const router = createBrowserRouter([
+ {
+ path: '/',
+ element: ,
+ errorElement: ,
+ children: [
+ {
+ index: true,
+ element: ,
+ },
+ {
+ path: 'register',
+ element: ,
+ },
+ {
+ path: 'login',
+ element: ,
+ },
+ {
+ path: 'dashboard',
+ element: ,
+ children: [
+ {
+ index: true,
+ element: ,
+ },
+ {
+ path: 'ranking',
+ element: ,
+ },
+ {
+ path: 'stats',
+ element: ,
+ },
+ {
+ path: 'profile',
+ element: ,
+ },
+ {
+ path: 'admin',
+ element: ,
+ },
+ ],
+ },
+ ],
+ },
+]);
+
+const App = () => {
+ return ;
+};
+export default App;
diff --git a/webapp/src/assets/images/logo.png b/webapp/src/assets/images/logo.png
new file mode 100644
index 0000000..12878a6
Binary files /dev/null and b/webapp/src/assets/images/logo.png differ
diff --git a/webapp/src/assets/images/main.svg b/webapp/src/assets/images/main.svg
new file mode 100644
index 0000000..5bcc40f
--- /dev/null
+++ b/webapp/src/assets/images/main.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/webapp/src/assets/images/not-found.svg b/webapp/src/assets/images/not-found.svg
new file mode 100644
index 0000000..f9f5212
--- /dev/null
+++ b/webapp/src/assets/images/not-found.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/webapp/src/assets/react.svg b/webapp/src/assets/react.svg
new file mode 100644
index 0000000..6c87de9
--- /dev/null
+++ b/webapp/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/webapp/src/assets/wrappers/BigSidebar.js b/webapp/src/assets/wrappers/BigSidebar.js
new file mode 100644
index 0000000..7e5cd7e
--- /dev/null
+++ b/webapp/src/assets/wrappers/BigSidebar.js
@@ -0,0 +1,62 @@
+import styled from 'styled-components';
+
+const Wrapper = styled.aside`
+ display: none;
+ @media (min-width: 992px) {
+ display: block;
+ box-shadow: 1px 0px 0px 0px rgba(0, 0, 0, 0.1);
+ .sidebar-container {
+ background: var(--background-secondary-color);
+ min-height: 100vh;
+ height: 100%;
+ width: 250px;
+ margin-left: -250px;
+ transition: margin-left 0.3s ease-in-out;
+ }
+ .content {
+ position: sticky;
+ top: 0;
+ }
+ .show-sidebar {
+ margin-left: 0;
+ }
+ header {
+ height: 6rem;
+ display: flex;
+ align-items: center;
+ padding-left: 2.5rem;
+ }
+ .nav-links {
+ padding-top: 2rem;
+ display: flex;
+ flex-direction: column;
+ }
+ .nav-link {
+ display: flex;
+ align-items: center;
+ color: var(--text-secondary-color);
+ padding: 1rem 0;
+ padding-left: 2.5rem;
+ text-transform: capitalize;
+ transition: padding-left 0.3s ease-in-out;
+ }
+ .nav-link:hover {
+ padding-left: 3rem;
+ color: var(--primary-500);
+ transition: var(--transition);
+ }
+ .icon {
+ font-size: 1.5rem;
+ margin-right: 1rem;
+ display: grid;
+ place-items: center;
+ }
+ .active {
+ color: var(--primary-500);
+ }
+ .pending {
+ background: var(--background-color);
+ }
+ }
+`;
+export default Wrapper;
diff --git a/webapp/src/assets/wrappers/ChartsContainer.js b/webapp/src/assets/wrappers/ChartsContainer.js
new file mode 100644
index 0000000..b7d524f
--- /dev/null
+++ b/webapp/src/assets/wrappers/ChartsContainer.js
@@ -0,0 +1,20 @@
+import styled from 'styled-components';
+
+const Wrapper = styled.section`
+ margin-top: 4rem;
+ text-align: center;
+ button {
+ background: transparent;
+ border-color: transparent;
+ text-transform: capitalize;
+ color: var(--primary-500);
+ font-size: 1.25rem;
+ cursor: pointer;
+ }
+ h4 {
+ text-align: center;
+ margin-bottom: 0.75rem;
+ }
+`;
+
+export default Wrapper;
diff --git a/webapp/src/assets/wrappers/Dashboard.js b/webapp/src/assets/wrappers/Dashboard.js
new file mode 100644
index 0000000..34a114e
--- /dev/null
+++ b/webapp/src/assets/wrappers/Dashboard.js
@@ -0,0 +1,22 @@
+import styled from 'styled-components';
+
+const Wrapper = styled.section`
+ .dashboard {
+ display: grid;
+ grid-template-columns: 1fr;
+ }
+ .dashboard-page {
+ width: 90vw;
+ margin: 0 auto;
+ padding: 2rem 0;
+ }
+ @media (min-width: 992px) {
+ .dashboard {
+ grid-template-columns: auto 1fr;
+ }
+ .dashboard-page {
+ width: 90%;
+ }
+ }
+`;
+export default Wrapper;
diff --git a/webapp/src/assets/wrappers/DashboardFormPage.js b/webapp/src/assets/wrappers/DashboardFormPage.js
new file mode 100644
index 0000000..d4d72b8
--- /dev/null
+++ b/webapp/src/assets/wrappers/DashboardFormPage.js
@@ -0,0 +1,46 @@
+import styled from 'styled-components';
+
+const Wrapper = styled.section`
+ border-radius: var(--border-radius);
+ width: 100%;
+ background: var(--background-secondary-color);
+ padding: 3rem 2rem 4rem;
+ .form-title {
+ margin-bottom: 2rem;
+ }
+ .form {
+ margin: 0;
+ border-radius: 0;
+ box-shadow: none;
+ padding: 0;
+ max-width: 100%;
+ width: 100%;
+ }
+ .form-row {
+ margin-bottom: 0;
+ }
+ .form-center {
+ display: grid;
+ row-gap: 1rem;
+ }
+ .form-btn {
+ align-self: end;
+ margin-top: 1rem;
+ display: grid;
+ place-items: center;
+ }
+ @media (min-width: 992px) {
+ .form-center {
+ grid-template-columns: 1fr 1fr;
+ align-items: center;
+ column-gap: 1rem;
+ }
+ }
+ @media (min-width: 1120px) {
+ .form-center {
+ grid-template-columns: 1fr 1fr 1fr;
+ }
+ }
+`;
+
+export default Wrapper;
diff --git a/webapp/src/assets/wrappers/ErrorPage.js b/webapp/src/assets/wrappers/ErrorPage.js
new file mode 100644
index 0000000..1630cdd
--- /dev/null
+++ b/webapp/src/assets/wrappers/ErrorPage.js
@@ -0,0 +1,31 @@
+import styled from 'styled-components';
+
+const Wrapper = styled.main`
+ min-height: 100vh;
+ text-align: center;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ img {
+ width: 90vw;
+ max-width: 600px;
+ display: block;
+ margin-bottom: 2rem;
+ margin-top: -3rem;
+ }
+ h3 {
+ margin-bottom: 0.5rem;
+ }
+ p {
+ line-height: 1.5;
+ margin-top: 0.5rem;
+ margin-bottom: 1rem;
+ color: var(--text-secondary-color);
+ }
+ a {
+ color: var(--primary-500);
+ text-transform: capitalize;
+ }
+`;
+
+export default Wrapper;
diff --git a/webapp/src/assets/wrappers/Job.js b/webapp/src/assets/wrappers/Job.js
new file mode 100644
index 0000000..79e5ad7
--- /dev/null
+++ b/webapp/src/assets/wrappers/Job.js
@@ -0,0 +1,81 @@
+import styled from 'styled-components';
+
+const Wrapper = styled.article`
+ background: var(--background-secondary-color);
+ border-radius: var(--border-radius);
+ display: grid;
+ grid-template-rows: 1fr auto;
+ box-shadow: var(--shadow-2);
+ header {
+ padding: 1rem 1.5rem;
+ border-bottom: 1px solid var(--grey-100);
+ display: grid;
+ grid-template-columns: auto 1fr;
+ align-items: center;
+ }
+ .main-icon {
+ width: 60px;
+ height: 60px;
+ display: grid;
+ place-items: center;
+ background: var(--primary-500);
+ border-radius: var(--border-radius);
+ font-size: 1.5rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ color: var(--white);
+ margin-right: 2rem;
+ }
+ .info {
+ h5 {
+ margin-bottom: 0.5rem;
+ }
+ p {
+ margin: 0;
+ text-transform: capitalize;
+ letter-spacing: var(--letter-spacing);
+ color: var(--text-secondary-color);
+ }
+ }
+ .content {
+ padding: 1rem 1.5rem;
+ }
+ .content-center {
+ display: grid;
+ margin-top: 1rem;
+ margin-bottom: 1.5rem;
+ grid-template-columns: 1fr;
+ row-gap: 1.5rem;
+ align-items: center;
+ @media (min-width: 576px) {
+ grid-template-columns: 1fr 1fr;
+ }
+ }
+ .status {
+ border-radius: var(--border-radius);
+ text-transform: capitalize;
+ letter-spacing: var(--letter-spacing);
+ text-align: center;
+ width: 100px;
+ height: 30px;
+ display: grid;
+ align-items: center;
+ }
+ .actions {
+ margin-top: 1rem;
+ display: flex;
+ align-items: center;
+ }
+ .edit-btn,
+ .delete-btn {
+ height: 30px;
+ font-size: 0.85rem;
+ display: flex;
+ align-items: center;
+ }
+ .edit-btn {
+ margin-right: 0.5rem;
+ }
+`;
+
+export default Wrapper;
diff --git a/webapp/src/assets/wrappers/JobInfo.js b/webapp/src/assets/wrappers/JobInfo.js
new file mode 100644
index 0000000..9762d73
--- /dev/null
+++ b/webapp/src/assets/wrappers/JobInfo.js
@@ -0,0 +1,20 @@
+import styled from 'styled-components';
+
+const Wrapper = styled.div`
+ display: flex;
+ align-items: center;
+ .job-icon {
+ font-size: 1rem;
+ margin-right: 1rem;
+ display: flex;
+ align-items: center;
+ svg {
+ color: var(--text-secondary-color);
+ }
+ }
+ .job-text {
+ text-transform: capitalize;
+ letter-spacing: var(--letter-spacing);
+ }
+`;
+export default Wrapper;
diff --git a/webapp/src/assets/wrappers/JobsContainer.js b/webapp/src/assets/wrappers/JobsContainer.js
new file mode 100644
index 0000000..1614441
--- /dev/null
+++ b/webapp/src/assets/wrappers/JobsContainer.js
@@ -0,0 +1,24 @@
+import styled from 'styled-components';
+
+const Wrapper = styled.section`
+ margin-top: 4rem;
+ h2 {
+ text-transform: none;
+ }
+ & > h5 {
+ font-weight: 700;
+ margin-bottom: 1.5rem;
+ }
+ .jobs {
+ display: grid;
+ grid-template-columns: 1fr;
+ row-gap: 2rem;
+ }
+ @media (min-width: 1120px) {
+ .jobs {
+ grid-template-columns: 1fr 1fr;
+ gap: 2rem;
+ }
+ }
+`;
+export default Wrapper;
diff --git a/webapp/src/assets/wrappers/LandingPage.js b/webapp/src/assets/wrappers/LandingPage.js
new file mode 100644
index 0000000..ee6c78a
--- /dev/null
+++ b/webapp/src/assets/wrappers/LandingPage.js
@@ -0,0 +1,50 @@
+import styled from 'styled-components';
+
+const Wrapper = styled.section`
+ nav {
+ width: var(--fluid-width);
+ max-width: var(--max-width);
+ margin: 0 auto;
+ height: var(--nav-height);
+ display: flex;
+ align-items: center;
+ }
+ .page {
+ min-height: calc(100vh - var(--nav-height));
+ display: grid;
+ align-items: center;
+ margin-top: -3rem;
+ }
+ h1 {
+ font-weight: 700;
+ span {
+ color: var(--primary-500);
+ }
+ margin-bottom: 1.5rem;
+ }
+ p {
+ line-height: 2;
+ color: var(--text-secondary-color);
+ margin-bottom: 1.5rem;
+ max-width: 35em;
+ }
+ .register-link {
+ margin-right: 1rem;
+ }
+ .main-img {
+ display: none;
+ }
+ .btn {
+ padding: 0.75rem 1rem;
+ }
+ @media (min-width: 992px) {
+ .page {
+ grid-template-columns: 1fr 400px;
+ column-gap: 3rem;
+ }
+ .main-img {
+ display: block;
+ }
+ }
+`;
+export default Wrapper;
diff --git a/webapp/src/assets/wrappers/LogoutContainer.js b/webapp/src/assets/wrappers/LogoutContainer.js
new file mode 100644
index 0000000..b2302ad
--- /dev/null
+++ b/webapp/src/assets/wrappers/LogoutContainer.js
@@ -0,0 +1,44 @@
+import styled from 'styled-components';
+
+const Wrapper = styled.div`
+ position: relative;
+ .logout-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0 0.5rem;
+ }
+ .img {
+ width: 25px;
+ height: 25px;
+ border-radius: 50%;
+ }
+ .dropdown {
+ position: absolute;
+ top: 45px;
+ left: 0;
+ width: 100%;
+ box-shadow: var(--shadow-2);
+ text-align: center;
+ visibility: hidden;
+ border-radius: var(--border-radius);
+ background: var(--primary-500);
+ }
+ .show-dropdown {
+ visibility: visible;
+ }
+ .dropdown-btn {
+ border-radius: var(--border-radius);
+ padding: 0.5rem;
+ background: transparent;
+ border-color: transparent;
+ color: var(--white);
+ letter-spacing: var(--letter-spacing);
+ text-transform: capitalize;
+ cursor: pointer;
+ width: 100%;
+ height: 100%;
+ }
+`;
+
+export default Wrapper;
diff --git a/webapp/src/assets/wrappers/Navbar.js b/webapp/src/assets/wrappers/Navbar.js
new file mode 100644
index 0000000..ef93da3
--- /dev/null
+++ b/webapp/src/assets/wrappers/Navbar.js
@@ -0,0 +1,51 @@
+import styled from 'styled-components';
+
+const Wrapper = styled.nav`
+ height: var(--nav-height);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.1);
+ background: var(--background-secondary-color);
+ .nav-center {
+ display: flex;
+ width: 90vw;
+ align-items: center;
+ justify-content: space-between;
+ }
+ .toggle-btn {
+ background: transparent;
+ border-color: transparent;
+ font-size: 1.75rem;
+ color: var(--primary-500);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ }
+ .logo-text {
+ display: none;
+ }
+ .logo {
+ display: flex;
+ align-items: center;
+ width: 100px;
+ }
+ .btn-container {
+ display: flex;
+ align-items: center;
+ }
+ @media (min-width: 992px) {
+ position: sticky;
+ top: 0;
+ .nav-center {
+ width: 90%;
+ }
+ .logo {
+ display: none;
+ }
+ .logo-text {
+ display: block;
+ }
+ }
+`;
+export default Wrapper;
diff --git a/webapp/src/assets/wrappers/PageBtnContainer.js b/webapp/src/assets/wrappers/PageBtnContainer.js
new file mode 100644
index 0000000..52600b1
--- /dev/null
+++ b/webapp/src/assets/wrappers/PageBtnContainer.js
@@ -0,0 +1,59 @@
+import styled from 'styled-components';
+
+const Wrapper = styled.section`
+ height: 6rem;
+ margin-top: 2rem;
+ display: flex;
+ align-items: center;
+ justify-content: end;
+ flex-wrap: wrap;
+ gap: 1rem;
+ .btn-container {
+ background: var(--background-secondary-color);
+ border-radius: var(--border-radius);
+ display: flex;
+ }
+ .page-btn {
+ background: transparent;
+ border-color: transparent;
+ width: 50px;
+ height: 40px;
+ font-weight: 700;
+ font-size: 1.25rem;
+ color: var(--primary-500);
+ border-radius: var(--border-radius);
+ cursor:pointer:
+ }
+ .active{
+ background:var(--primary-500);
+ color: var(--white);
+
+ }
+ .prev-btn,.next-btn{
+ background: var(--background-secondary-color);
+ border-color: transparent;
+ border-radius: var(--border-radius);
+
+ width: 100px;
+ height: 40px;
+ color: var(--primary-500);
+text-transform:capitalize;
+letter-spacing:var(--letter-spacing);
+display:flex;
+align-items:center;
+justify-content:center;
+gap:0.5rem;
+cursor:pointer;
+ }
+ .prev-btn:hover,.next-btn:hover{
+ background:var(--primary-500);
+ color: var(--white);
+ transition:var(--transition);
+ }
+.dots{
+ display:grid;
+ place-items:center;
+ cursor:text;
+}
+`;
+export default Wrapper;
diff --git a/webapp/src/assets/wrappers/RegisterAndLoginPage.js b/webapp/src/assets/wrappers/RegisterAndLoginPage.js
new file mode 100644
index 0000000..b8ee0d9
--- /dev/null
+++ b/webapp/src/assets/wrappers/RegisterAndLoginPage.js
@@ -0,0 +1,34 @@
+import styled from 'styled-components';
+
+const Wrapper = styled.section`
+ min-height: 100vh;
+ display: grid;
+ align-items: center;
+ .logo {
+ display: block;
+ margin: 0 auto;
+ margin-bottom: 1.38rem;
+ }
+ .form {
+ max-width: 400px;
+ border-top: 5px solid var(--primary-500);
+ }
+ h4 {
+ text-align: center;
+ margin-bottom: 1.38rem;
+ }
+ p {
+ margin-top: 1rem;
+ text-align: center;
+ line-height: 1.5;
+ }
+ .btn {
+ margin-top: 1rem;
+ }
+ .member-btn {
+ color: var(--primary-500);
+ letter-spacing: var(--letter-spacing);
+ margin-left: 0.25rem;
+ }
+`;
+export default Wrapper;
diff --git a/webapp/src/assets/wrappers/SmallSidebar.js b/webapp/src/assets/wrappers/SmallSidebar.js
new file mode 100644
index 0000000..5a6d2cf
--- /dev/null
+++ b/webapp/src/assets/wrappers/SmallSidebar.js
@@ -0,0 +1,71 @@
+import styled from 'styled-components';
+
+const Wrapper = styled.aside`
+ @media (min-width: 992px) {
+ display: none;
+ }
+ .sidebar-container {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.7);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: -1;
+ opacity: 0;
+ transition: var(--transition);
+ visibility: hidden;
+ }
+ .show-sidebar {
+ z-index: 99;
+ opacity: 1;
+ visibility: visible;
+ }
+ .content {
+ background: var(--background-secondary-color);
+ width: var(--fluid-width);
+ height: 95vh;
+ border-radius: var(--border-radius);
+ padding: 4rem 2rem;
+ position: relative;
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+ }
+ .close-btn {
+ position: absolute;
+ top: 10px;
+ left: 10px;
+ background: transparent;
+ border-color: transparent;
+ font-size: 2rem;
+ color: var(--red-dark);
+ cursor: pointer;
+ }
+ .nav-links {
+ padding-top: 2rem;
+ display: flex;
+ flex-direction: column;
+ }
+ .nav-link {
+ display: flex;
+ align-items: center;
+ color: var(--text-secondary-color);
+ padding: 1rem 0;
+ text-transform: capitalize;
+ transition: var(--transition);
+ }
+ .nav-link:hover {
+ color: var(--primary-500);
+ }
+ .icon {
+ font-size: 1.5rem;
+ margin-right: 1rem;
+ display: grid;
+ place-items: center;
+ }
+ .active {
+ color: var(--primary-500);
+ }
+`;
+export default Wrapper;
diff --git a/webapp/src/assets/wrappers/StatItem.js b/webapp/src/assets/wrappers/StatItem.js
new file mode 100644
index 0000000..126db83
--- /dev/null
+++ b/webapp/src/assets/wrappers/StatItem.js
@@ -0,0 +1,44 @@
+import styled from 'styled-components';
+
+const Wrapper = styled.article`
+ padding: 2rem;
+ background: var(--background-secondary-color);
+ border-bottom: 5px solid ${(props) => props.color};
+ border-radius: var(--border-radius);
+
+ header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+ .count {
+ display: block;
+ font-weight: 700;
+ font-size: 50px;
+ color: ${(props) => props.color};
+ line-height: 2;
+ }
+ .title {
+ margin: 0;
+ text-transform: capitalize;
+ letter-spacing: var(--letter-spacing);
+ text-align: left;
+ margin-top: 0.5rem;
+ font-size: 1.25rem;
+ }
+ .icon {
+ width: 70px;
+ height: 60px;
+ background: ${(props) => props.bcg};
+ border-radius: var(--border-radius);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ svg {
+ font-size: 2rem;
+ color: ${(props) => props.color};
+ }
+ }
+`;
+
+export default Wrapper;
diff --git a/webapp/src/assets/wrappers/StatsContainer.js b/webapp/src/assets/wrappers/StatsContainer.js
new file mode 100644
index 0000000..e55e514
--- /dev/null
+++ b/webapp/src/assets/wrappers/StatsContainer.js
@@ -0,0 +1,14 @@
+import styled from 'styled-components';
+
+const Wrapper = styled.section`
+ display: grid;
+ row-gap: 2rem;
+ @media (min-width: 768px) {
+ grid-template-columns: 1fr 1fr;
+ column-gap: 1rem;
+ }
+ @media (min-width: 1120px) {
+ grid-template-columns: 1fr 1fr 1fr;
+ }
+`;
+export default Wrapper;
diff --git a/webapp/src/assets/wrappers/Testing.js b/webapp/src/assets/wrappers/Testing.js
new file mode 100644
index 0000000..d412e04
--- /dev/null
+++ b/webapp/src/assets/wrappers/Testing.js
@@ -0,0 +1,40 @@
+import styled from 'styled-components'
+
+const Wrapper = styled.main`
+ nav {
+ width: var(--fluid-width);
+ max-width: var(--max-width);
+ margin: 0 auto;
+ height: var(--nav-height);
+ display: flex;
+ align-items: center;
+ }
+ .page {
+ min-height: calc(100vh - var(--nav-height));
+ display: grid;
+ align-items: center;
+ margin-top: -3rem;
+ }
+ h1 {
+ font-weight: 700;
+ span {
+ color: var(--primary-500);
+ }
+ }
+ p {
+ color: var(--grey-600);
+ }
+ .main-img {
+ display: none;
+ }
+ @media (min-width: 992px) {
+ .page {
+ grid-template-columns: 1fr 1fr;
+ column-gap: 3rem;
+ }
+ .main-img {
+ display: block;
+ }
+ }
+`
+export default Wrapper
diff --git a/webapp/src/assets/wrappers/ThemeToggle.js b/webapp/src/assets/wrappers/ThemeToggle.js
new file mode 100644
index 0000000..cee0612
--- /dev/null
+++ b/webapp/src/assets/wrappers/ThemeToggle.js
@@ -0,0 +1,16 @@
+import styled from 'styled-components';
+
+const Wrapper = styled.button`
+ background: transparent;
+ border-color: transparent;
+ width: 3.5rem;
+ height: 2rem;
+ display: grid;
+ place-items: center;
+ cursor: pointer;
+ .toggle-icon {
+ font-size: 1.15rem;
+ color: var(--text-color);
+ }
+`;
+export default Wrapper;
diff --git a/webapp/src/components/AddUser.js b/webapp/src/components/AddUser.js
deleted file mode 100644
index 00d522a..0000000
--- a/webapp/src/components/AddUser.js
+++ /dev/null
@@ -1,60 +0,0 @@
-// src/components/AddUser.js
-import React, { useState } from 'react';
-import axios from 'axios';
-import { Container, Typography, TextField, Button, Snackbar } from '@mui/material';
-
-const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000';
-
-const AddUser = () => {
- const [username, setUsername] = useState('');
- const [password, setPassword] = useState('');
- const [error, setError] = useState('');
- const [openSnackbar, setOpenSnackbar] = useState(false);
-
- const addUser = async () => {
- try {
- await axios.post(`${apiEndpoint}/adduser`, { username, password });
- setOpenSnackbar(true);
- } catch (error) {
- setError(error.response.data.error);
- }
- };
-
- const handleCloseSnackbar = () => {
- setOpenSnackbar(false);
- };
-
- return (
-
-
- Add User
-
- setUsername(e.target.value)}
- />
- setPassword(e.target.value)}
- />
-
- Add User
-
-
- {error && (
- setError('')} message={`Error: ${error}`} />
- )}
-
- );
-};
-
-export default AddUser;
diff --git a/webapp/src/components/AddUser.test.js b/webapp/src/components/AddUser.test.js
deleted file mode 100644
index 8733488..0000000
--- a/webapp/src/components/AddUser.test.js
+++ /dev/null
@@ -1,59 +0,0 @@
-import React from 'react';
-import { render, fireEvent, screen, waitFor } from '@testing-library/react';
-import axios from 'axios';
-import MockAdapter from 'axios-mock-adapter';
-import AddUser from './AddUser';
-
-const mockAxios = new MockAdapter(axios);
-
-describe('AddUser component', () => {
- beforeEach(() => {
- mockAxios.reset();
- });
-
- it('should add user successfully', async () => {
- render( );
-
- const usernameInput = screen.getByLabelText(/Username/i);
- const passwordInput = screen.getByLabelText(/Password/i);
- const addUserButton = screen.getByRole('button', { name: /Add User/i });
-
- // Mock the axios.post request to simulate a successful response
- mockAxios.onPost('http://localhost:8000/adduser').reply(200);
-
- // Simulate user input
- fireEvent.change(usernameInput, { target: { value: 'testUser' } });
- fireEvent.change(passwordInput, { target: { value: 'testPassword' } });
-
- // Trigger the add user button click
- fireEvent.click(addUserButton);
-
- // Wait for the Snackbar to be open
- await waitFor(() => {
- expect(screen.getByText(/User added successfully/i)).toBeInTheDocument();
- });
- });
-
- it('should handle error when adding user', async () => {
- render( );
-
- const usernameInput = screen.getByLabelText(/Username/i);
- const passwordInput = screen.getByLabelText(/Password/i);
- const addUserButton = screen.getByRole('button', { name: /Add User/i });
-
- // Mock the axios.post request to simulate an error response
- mockAxios.onPost('http://localhost:8000/adduser').reply(500, { error: 'Internal Server Error' });
-
- // Simulate user input
- fireEvent.change(usernameInput, { target: { value: 'testUser' } });
- fireEvent.change(passwordInput, { target: { value: 'testPassword' } });
-
- // Trigger the add user button click
- fireEvent.click(addUserButton);
-
- // Wait for the error Snackbar to be open
- await waitFor(() => {
- expect(screen.getByText(/Error: Internal Server Error/i)).toBeInTheDocument();
- });
- });
-});
diff --git a/webapp/src/components/BigSidebar.jsx b/webapp/src/components/BigSidebar.jsx
new file mode 100644
index 0000000..1398b2e
--- /dev/null
+++ b/webapp/src/components/BigSidebar.jsx
@@ -0,0 +1,26 @@
+import Wrapper from '../assets/wrappers/BigSidebar';
+import NavLinks from './NavLinks';
+import Logo from './Logo';
+import { useDashboardContext } from '../pages/DashboardLayout';
+
+const BigSidebar = () => {
+ const { showSidebar } = useDashboardContext();
+ return (
+
+
+
+ );
+};
+
+export default BigSidebar;
diff --git a/webapp/src/components/FormRow.jsx b/webapp/src/components/FormRow.jsx
new file mode 100644
index 0000000..0d42530
--- /dev/null
+++ b/webapp/src/components/FormRow.jsx
@@ -0,0 +1,21 @@
+const FormRow = ({ type, name, labelText, value, defaultValue, onChange }) => {
+ return (
+
+
+ {labelText || name}
+
+
+
+ );
+};
+
+export default FormRow;
diff --git a/webapp/src/components/Login.js b/webapp/src/components/Login.js
deleted file mode 100644
index 0ad6268..0000000
--- a/webapp/src/components/Login.js
+++ /dev/null
@@ -1,80 +0,0 @@
-// src/components/Login.js
-import React, { useState } from 'react';
-import axios from 'axios';
-import { Container, Typography, TextField, Button, Snackbar } from '@mui/material';
-
-const Login = () => {
- const [username, setUsername] = useState('');
- const [password, setPassword] = useState('');
- const [error, setError] = useState('');
- const [loginSuccess, setLoginSuccess] = useState(false);
- const [createdAt, setCreatedAt] = useState('');
- const [openSnackbar, setOpenSnackbar] = useState(false);
-
- const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000';
-
- const loginUser = async () => {
- try {
- const response = await axios.post(`${apiEndpoint}/login`, { username, password });
-
- // Extract data from the response
- const { createdAt: userCreatedAt } = response.data;
-
- setCreatedAt(userCreatedAt);
- setLoginSuccess(true);
-
- setOpenSnackbar(true);
- } catch (error) {
- setError(error.response.data.error);
- }
- };
-
- const handleCloseSnackbar = () => {
- setOpenSnackbar(false);
- };
-
- return (
-
- {loginSuccess ? (
-
-
- Hello {username}!
-
-
- Your account was created on {new Date(createdAt).toLocaleDateString()}.
-
-
- ) : (
-
-
- Login
-
- setUsername(e.target.value)}
- />
- setPassword(e.target.value)}
- />
-
- Login
-
-
- {error && (
- setError('')} message={`Error: ${error}`} />
- )}
-
- )}
-
- );
-};
-
-export default Login;
diff --git a/webapp/src/components/Login.test.js b/webapp/src/components/Login.test.js
deleted file mode 100644
index af102dc..0000000
--- a/webapp/src/components/Login.test.js
+++ /dev/null
@@ -1,62 +0,0 @@
-import React from 'react';
-import { render, fireEvent, screen, waitFor, act } from '@testing-library/react';
-import axios from 'axios';
-import MockAdapter from 'axios-mock-adapter';
-import Login from './Login';
-
-const mockAxios = new MockAdapter(axios);
-
-describe('Login component', () => {
- beforeEach(() => {
- mockAxios.reset();
- });
-
- it('should log in successfully', async () => {
- render( );
-
- const usernameInput = screen.getByLabelText(/Username/i);
- const passwordInput = screen.getByLabelText(/Password/i);
- const loginButton = screen.getByRole('button', { name: /Login/i });
-
- // Mock the axios.post request to simulate a successful response
- mockAxios.onPost('http://localhost:8000/login').reply(200, { createdAt: '2024-01-01T12:34:56Z' });
-
- // Simulate user input
- await act(async () => {
- fireEvent.change(usernameInput, { target: { value: 'testUser' } });
- fireEvent.change(passwordInput, { target: { value: 'testPassword' } });
- fireEvent.click(loginButton);
- });
-
- // Verify that the user information is displayed
- expect(screen.getByText(/Hello testUser!/i)).toBeInTheDocument();
- expect(screen.getByText(/Your account was created on 1\/1\/2024/i)).toBeInTheDocument();
- });
-
- it('should handle error when logging in', async () => {
- render( );
-
- const usernameInput = screen.getByLabelText(/Username/i);
- const passwordInput = screen.getByLabelText(/Password/i);
- const loginButton = screen.getByRole('button', { name: /Login/i });
-
- // Mock the axios.post request to simulate an error response
- mockAxios.onPost('http://localhost:8000/login').reply(401, { error: 'Unauthorized' });
-
- // Simulate user input
- fireEvent.change(usernameInput, { target: { value: 'testUser' } });
- fireEvent.change(passwordInput, { target: { value: 'testPassword' } });
-
- // Trigger the login button click
- fireEvent.click(loginButton);
-
- // Wait for the error Snackbar to be open
- await waitFor(() => {
- expect(screen.getByText(/Error: Unauthorized/i)).toBeInTheDocument();
- });
-
- // Verify that the user information is not displayed
- expect(screen.queryByText(/Hello testUser!/i)).toBeNull();
- expect(screen.queryByText(/Your account was created on/i)).toBeNull();
- });
-});
diff --git a/webapp/src/components/Logo.jsx b/webapp/src/components/Logo.jsx
new file mode 100644
index 0000000..2d909a3
--- /dev/null
+++ b/webapp/src/components/Logo.jsx
@@ -0,0 +1,7 @@
+import logo from '../assets/images/logo.png';
+
+const Logo = () => {
+ return ;
+};
+
+export default Logo;
diff --git a/webapp/src/components/LogoutContainer.jsx b/webapp/src/components/LogoutContainer.jsx
new file mode 100644
index 0000000..816ad2e
--- /dev/null
+++ b/webapp/src/components/LogoutContainer.jsx
@@ -0,0 +1,29 @@
+import { FaUserCircle, FaCaretDown } from 'react-icons/fa';
+import Wrapper from '../assets/wrappers/LogoutContainer';
+import { useState } from 'react';
+import { useDashboardContext } from '../pages/DashboardLayout';
+
+const LogoutContainer = () => {
+ const [showLogout, setShowLogout] = useState(false);
+ const { user, logoutUser } = useDashboardContext();
+
+ return (
+
+ setShowLogout(!showLogout)}
+ >
+
+ {user?.name}
+
+
+
+
+ logout
+
+
+
+ );
+};
+export default LogoutContainer;
diff --git a/webapp/src/components/NavLinks.jsx b/webapp/src/components/NavLinks.jsx
new file mode 100644
index 0000000..25d2c16
--- /dev/null
+++ b/webapp/src/components/NavLinks.jsx
@@ -0,0 +1,28 @@
+import { useDashboardContext } from '../pages/DashboardLayout';
+import links from '../utils/links';
+import { NavLink } from 'react-router-dom';
+
+const NavLinks = ({ isBigSidebar }) => {
+ const { toggleSidebar, user } = useDashboardContext();
+
+ return (
+
+ {links.map((link) => {
+ const { text, path, icon } = link;
+ return (
+
+ {icon}
+ {text}
+
+ );
+ })}
+
+ );
+};
+export default NavLinks;
diff --git a/webapp/src/components/Navbar.jsx b/webapp/src/components/Navbar.jsx
new file mode 100644
index 0000000..c9398a8
--- /dev/null
+++ b/webapp/src/components/Navbar.jsx
@@ -0,0 +1,29 @@
+import Wrapper from '../assets/wrappers/Navbar';
+import { FaAlignLeft } from 'react-icons/fa';
+import Logo from './Logo';
+import { useDashboardContext } from '../pages/DashboardLayout';
+import LogoutContainer from './LogoutContainer';
+import ThemeToogle from './ThemeToogle';
+
+const Navbar = () => {
+ const { toggleSidebar } = useDashboardContext();
+ return (
+
+
+
+
+
+
+
+
dashboard
+
+
+
+
+
+
+
+ );
+};
+
+export default Navbar;
diff --git a/webapp/src/components/SmallSidebar.jsx b/webapp/src/components/SmallSidebar.jsx
new file mode 100644
index 0000000..a311b16
--- /dev/null
+++ b/webapp/src/components/SmallSidebar.jsx
@@ -0,0 +1,32 @@
+import Wrapper from '../assets/wrappers/SmallSidebar';
+import { useDashboardContext } from '../pages/DashboardLayout';
+import { FaTimes } from 'react-icons/fa';
+import Logo from './Logo';
+import links from '../utils/links';
+import { NavLink } from 'react-router-dom';
+import NavLinks from './NavLinks';
+
+const SmallSidebar = () => {
+ const { showSidebar, toggleSidebar } = useDashboardContext();
+
+ return (
+
+
+
+ );
+};
+export default SmallSidebar;
diff --git a/webapp/src/components/ThemeToogle.jsx b/webapp/src/components/ThemeToogle.jsx
new file mode 100644
index 0000000..df0824e
--- /dev/null
+++ b/webapp/src/components/ThemeToogle.jsx
@@ -0,0 +1,18 @@
+import { BsFillSunFill, BsFillMoonFill } from 'react-icons/bs';
+import Wrapper from '../assets/wrappers/ThemeToggle';
+import { useDashboardContext } from '../pages/DashboardLayout';
+
+const ThemeToogle = () => {
+ const { isDarkTheme, toggleDarkTheme } = useDashboardContext();
+
+ return (
+
+ {isDarkTheme ? (
+
+ ) : (
+
+ )}
+
+ );
+};
+export default ThemeToogle;
diff --git a/webapp/src/components/index.js b/webapp/src/components/index.js
new file mode 100644
index 0000000..b9c5889
--- /dev/null
+++ b/webapp/src/components/index.js
@@ -0,0 +1,5 @@
+export { default as Logo } from './Logo';
+export { default as FormRow } from './FormRow';
+export { default as BigSidebar } from './BigSidebar';
+export { default as SmallSidebar } from './SmallSidebar';
+export { default as Navbar } from './Navbar';
diff --git a/webapp/src/index.css b/webapp/src/index.css
index ec2585e..2be38f7 100644
--- a/webapp/src/index.css
+++ b/webapp/src/index.css
@@ -1,13 +1,332 @@
+/* ============= GLOBAL CSS =============== */
+
+*,
+::after,
+::before {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+html {
+ font-size: 100%;
+} /*16px*/
+
+:root {
+ /* colors */
+
+ --primary-50: #e5ffff;
+ --primary-100: #b3e5ff;
+ --primary-200: #b3e5ff;
+ --primary-300: #99ccff;
+ --primary-400: #80b2ff;
+ --primary-500: #6698eb;
+ --primary-600: #4d7ed8;
+ --primary-700: #3365c5;
+ --primary-800: #1a4bb3;
+ --primary-900: #0032a0;
+
+ /* grey */
+ --grey-50: #f8fafc;
+ --grey-100: #f1f5f9;
+ --grey-200: #e2e8f0;
+ --grey-300: #cbd5e1;
+ --grey-400: #94a3b8;
+ --grey-500: #64748b;
+ --grey-600: #475569;
+ --grey-700: #334155;
+ --grey-800: #1e293b;
+ --grey-900: #0f172a;
+ /* rest of the colors */
+ --black: #222;
+ --white: #fff;
+ --red-light: #f8d7da;
+ --red-dark: #842029;
+ --blue-light: #8dbfff;
+ --blue-dark: #0d3faa;
+
+ --small-text: 0.875rem;
+ --extra-small-text: 0.7em;
+ /* rest of the vars */
+
+ --border-radius: 0.25rem;
+ --letter-spacing: 1px;
+ --transition: 0.3s ease-in-out all;
+ --max-width: 1120px;
+ --fixed-width: 600px;
+ --fluid-width: 90vw;
+ --nav-height: 6rem;
+ /* box shadow*/
+ --shadow-1: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+ --shadow-2: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
+ 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+ --shadow-3: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
+ 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+ --shadow-4: 0 20px 25px -5px rgba(0, 0, 0, 0.1),
+ 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+ /* DARK MODE */
+
+ --dark-mode-bg-color: #333;
+ --dark-mode-text-color: #f0f0f0;
+ --dark-mode-bg-secondary-color: #3f3f3f;
+ --dark-mode-text-secondary-color: var(--grey-300);
+
+ --background-color: var(--grey-50);
+ --text-color: var(--grey-900);
+ --background-secondary-color: var(--white);
+ --text-secondary-color: var(--grey-500);
+}
+
+.dark-theme {
+ --text-color: var(--dark-mode-text-color);
+ --background-color: var(--dark-mode-bg-color);
+ --text-secondary-color: var(--dark-mode-text-secondary-color);
+ --background-secondary-color: var(--dark-mode-bg-secondary-color);
+}
+
body {
+ background: var(--background-color);
+ color: var(--text-color);
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
+ Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+ font-weight: 400;
+ line-height: 1;
+}
+p {
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;
+}
+h1,
+h2,
+h3,
+h4,
+h5 {
+ margin: 0;
+ font-weight: 400;
+ line-height: 1;
+ text-transform: capitalize;
+ letter-spacing: var(--letter-spacing);
+}
+
+h1 {
+ font-size: clamp(2rem, 5vw, 5rem); /* Large heading */
+}
+
+h2 {
+ font-size: clamp(1.5rem, 3vw, 3rem); /* Medium heading */
+}
+
+h3 {
+ font-size: clamp(1.25rem, 2.5vw, 2.5rem); /* Small heading */
+}
+
+h4 {
+ font-size: clamp(1rem, 2vw, 2rem); /* Extra small heading */
+}
+
+h5 {
+ font-size: clamp(0.875rem, 1.5vw, 1.5rem); /* Tiny heading */
+}
+
+/* BIGGER FONTS */
+/* h1 {
+ font-size: clamp(3rem, 6vw, 6rem);
+}
+
+h2 {
+ font-size: clamp(2.5rem, 5vw, 5rem);
+}
+
+h3 {
+ font-size: clamp(2rem, 4vw, 4rem);
+}
+
+h4 {
+ font-size: clamp(1.5rem, 3vw, 3rem);
+}
+
+h5 {
+ font-size: clamp(1rem, 2vw, 2rem);
+}
+ */
+
+.text {
+ margin-bottom: 1.5rem;
+ max-width: 40em;
+}
+
+small,
+.text-small {
+ font-size: var(--small-text);
}
-code {
- font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
- monospace;
+a {
+ text-decoration: none;
+}
+ul {
+ list-style-type: none;
+ padding: 0;
+}
+
+.img {
+ width: 100%;
+ display: block;
+ object-fit: cover;
+}
+/* buttons */
+
+.btn {
+ cursor: pointer;
+ color: var(--white);
+ background: var(--primary-500);
+ border: transparent;
+ border-radius: var(--border-radius);
+ letter-spacing: var(--letter-spacing);
+ padding: 0.375rem 0.75rem;
+ box-shadow: var(--shadow-1);
+ transition: var(--transition);
+ text-transform: capitalize;
+ display: inline-block;
+}
+.btn:hover {
+ background: var(--primary-700);
+ box-shadow: var(--shadow-3);
+}
+.btn-hipster {
+ color: var(--primary-500);
+ background: var(--primary-200);
+}
+.btn-hipster:hover {
+ color: var(--primary-200);
+ background: var(--primary-700);
+}
+.btn-block {
+ width: 100%;
+}
+button:disabled {
+ cursor: wait;
+}
+.danger-btn {
+ color: var(--red-dark);
+ background: var(--red-light);
+}
+.danger-btn:hover {
+ color: var(--white);
+ background: var(--red-dark);
+}
+/* alerts */
+.alert {
+ padding: 0.375rem 0.75rem;
+ margin-bottom: 1rem;
+ border-color: transparent;
+ border-radius: var(--border-radius);
+}
+
+.alert-danger {
+ color: var(--red-dark);
+ background: var(--red-light);
+}
+.alert-success {
+ color: var(--blue-dark);
+ background: var(--blue-light);
+}
+/* form */
+
+.form {
+ width: 90vw;
+ max-width: var(--fixed-width);
+ background: var(--background-secondary-color);
+ border-radius: var(--border-radius);
+ box-shadow: var(--shadow-2);
+ padding: 2rem 2.5rem;
+ margin: 3rem auto;
+}
+.form-label {
+ display: block;
+ font-size: var(--small-text);
+ margin-bottom: 0.75rem;
+ text-transform: capitalize;
+ letter-spacing: var(--letter-spacing);
+ line-height: 1.5;
+}
+.form-input,
+.form-textarea,
+.form-select {
+ width: 100%;
+ padding: 0.375rem 0.75rem;
+ border-radius: var(--border-radius);
+ background: var(--background-color);
+ border: 1px solid var(--grey-300);
+ color: var(--text-color);
+}
+.form-input,
+.form-select,
+.form-btn {
+ height: 35px;
+}
+.form-row {
+ margin-bottom: 1rem;
+}
+
+.form-textarea {
+ height: 7rem;
+}
+::placeholder {
+ font-family: inherit;
+ color: var(--grey-400);
+}
+.form-alert {
+ color: var(--red-dark);
+ letter-spacing: var(--letter-spacing);
+ text-transform: capitalize;
+}
+/* alert */
+
+@keyframes spinner {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.loading {
+ width: 6rem;
+ height: 6rem;
+ border: 5px solid var(--grey-400);
+ border-radius: 50%;
+ border-top-color: var(--primary-500);
+ animation: spinner 0.6s linear infinite;
+}
+
+/* title */
+
+.title {
+ text-align: center;
+}
+
+.title-underline {
+ background: var(--primary-500);
+ width: 7rem;
+ height: 0.25rem;
+ margin: 0 auto;
+ margin-top: 1rem;
+}
+
+.container {
+ width: var(--fluid-width);
+ max-width: var(--max-width);
+ margin: 0 auto;
+}
+
+/* BUTTONS AND BADGES */
+.pending {
+ background: #fef3c7;
+ color: #f59e0b;
+}
+
+.interview {
+ background: #e0e8f9;
+ color: #647acb;
+}
+.declined {
+ background: #ffeeee;
+ color: #d66a6a;
}
diff --git a/webapp/src/index.js b/webapp/src/index.js
index d563c0f..0bc4d9f 100644
--- a/webapp/src/index.js
+++ b/webapp/src/index.js
@@ -1,11 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
+import App from './App.jsx';
import './index.css';
-import App from './App';
import reportWebVitals from './reportWebVitals';
-const root = ReactDOM.createRoot(document.getElementById('root'));
-root.render(
+ReactDOM.createRoot(document.getElementById('root')).render(
diff --git a/webapp/src/logo.svg b/webapp/src/logo.svg
deleted file mode 100644
index 9dfc1c0..0000000
--- a/webapp/src/logo.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/webapp/src/pages/AddUser.jsx b/webapp/src/pages/AddUser.jsx
new file mode 100644
index 0000000..e483de6
--- /dev/null
+++ b/webapp/src/pages/AddUser.jsx
@@ -0,0 +1,110 @@
+import React, { useState } from 'react';
+import axios from 'axios';
+import { useNavigate, Link } from 'react-router-dom';
+import Wrapper from '../assets/wrappers/RegisterAndLoginPage';
+import { Logo, FormRow } from '../components';
+import { Snackbar } from '@mui/material';
+
+const apiEndpoint =
+ process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000';
+
+const AddUser = () => {
+ const [name, setName] = useState('');
+ const [lastName, setLastName] = useState('');
+ const [email, setEmail] = useState('');
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+ const [error, setError] = useState('');
+ const [openSnackbar, setOpenSnackbar] = useState(false);
+ const navigate = useNavigate();
+
+ const addUser = async () => {
+ console.log({ username });
+ console.log({ password });
+ try {
+ await axios.post(`${apiEndpoint}/adduser`, {
+ name,
+ lastName,
+ email,
+ username,
+ password,
+ });
+ setOpenSnackbar(true);
+ navigate('/login');
+ } catch (error) {
+ setError(error.response.data.error);
+ }
+ };
+
+ const handleCloseSnackbar = () => {
+ setOpenSnackbar(false);
+ };
+
+ return (
+
+
+
+ {error && (
+ setError('')}
+ message={`Error: ${error}`}
+ />
+ )}
+
+ );
+};
+
+export default AddUser;
diff --git a/webapp/src/pages/Admin.jsx b/webapp/src/pages/Admin.jsx
new file mode 100644
index 0000000..0dbb6de
--- /dev/null
+++ b/webapp/src/pages/Admin.jsx
@@ -0,0 +1,4 @@
+const Admin = () => {
+ return Admin Page ;
+};
+export default Admin;
diff --git a/webapp/src/pages/DashboardLayout.jsx b/webapp/src/pages/DashboardLayout.jsx
new file mode 100644
index 0000000..b8dd39e
--- /dev/null
+++ b/webapp/src/pages/DashboardLayout.jsx
@@ -0,0 +1,58 @@
+import { Outlet } from 'react-router-dom';
+import Wrapper from '../assets/wrappers/Dashboard';
+import { SmallSidebar, BigSidebar, Navbar } from '../components';
+import { createContext, useContext, useState } from 'react';
+import { checkDefaultTheme } from '../App';
+
+const DashboardContext = createContext();
+
+const DashboardLayout = () => {
+ // temp
+ const user = { name: 'user' };
+ const [showSidebar, setShowSidebar] = useState(false);
+ const [isDarkTheme, setIsDarkTheme] = useState(checkDefaultTheme());
+
+ const toggleDarkTheme = () => {
+ const newDarkTheme = !isDarkTheme;
+ setIsDarkTheme(newDarkTheme);
+ document.body.classList.toggle('dark-theme', newDarkTheme);
+ localStorage.setItem('darkTheme', newDarkTheme);
+ };
+
+ const toggleSidebar = () => {
+ setShowSidebar(!showSidebar);
+ };
+
+ const logoutUser = async () => {
+ console.log('logout user');
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export const useDashboardContext = () => useContext(DashboardContext);
+export default DashboardLayout;
diff --git a/webapp/src/pages/DeleteJob.jsx b/webapp/src/pages/DeleteJob.jsx
new file mode 100644
index 0000000..5ce5e60
--- /dev/null
+++ b/webapp/src/pages/DeleteJob.jsx
@@ -0,0 +1,4 @@
+const DeleteJob = () => {
+ return DeleteJob Page ;
+};
+export default DeleteJob;
diff --git a/webapp/src/pages/EditJob.jsx b/webapp/src/pages/EditJob.jsx
new file mode 100644
index 0000000..4563aec
--- /dev/null
+++ b/webapp/src/pages/EditJob.jsx
@@ -0,0 +1,4 @@
+const EditJob = () => {
+ return EditJob Page ;
+};
+export default EditJob;
diff --git a/webapp/src/pages/Error.jsx b/webapp/src/pages/Error.jsx
new file mode 100644
index 0000000..c3da61c
--- /dev/null
+++ b/webapp/src/pages/Error.jsx
@@ -0,0 +1,28 @@
+import { Link, useRouteError } from 'react-router-dom';
+import Wrapper from '../assets/wrappers/ErrorPage';
+import img from '../assets/images/not-found.svg';
+
+const Error = () => {
+ const error = useRouteError();
+
+ if (error.status === 404) {
+ return (
+
+
+
+
Page not found!
+
We can not find the page you are looking for
+
back home
+
+
+ );
+ }
+ return (
+
+
+
Something went wrong
+
+
+ );
+};
+export default Error;
diff --git a/webapp/src/pages/HomeLayout.jsx b/webapp/src/pages/HomeLayout.jsx
new file mode 100644
index 0000000..728a067
--- /dev/null
+++ b/webapp/src/pages/HomeLayout.jsx
@@ -0,0 +1,10 @@
+import { Outlet } from 'react-router-dom';
+
+const HomeLayout = () => {
+ return (
+
+
+
+ );
+};
+export default HomeLayout;
diff --git a/webapp/src/pages/Landing.jsx b/webapp/src/pages/Landing.jsx
new file mode 100644
index 0000000..b8d3b62
--- /dev/null
+++ b/webapp/src/pages/Landing.jsx
@@ -0,0 +1,36 @@
+import Wrapper from '../assets/wrappers/LandingPage';
+import main from '../assets/images/main.svg';
+import { Link } from 'react-router-dom';
+import { Logo } from '../components';
+
+const Landing = () => {
+ return (
+
+
+
+
+
+
+
+ wiq quiz game
+
+
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Tenetur
+ quas quidem itaque doloremque accusantium laudantium? Earum maiores
+ consectetur vero eligendi, beatae reprehenderit recusandae placeat
+ accusantium dignissimos deserunt sed quasi quisquam!
+
+
+ Register
+
+
+ Login / Demo User
+
+
+
+
+
+ );
+};
+
+export default Landing;
diff --git a/webapp/src/pages/Login.jsx b/webapp/src/pages/Login.jsx
new file mode 100644
index 0000000..fac47d3
--- /dev/null
+++ b/webapp/src/pages/Login.jsx
@@ -0,0 +1,79 @@
+import React, { useState } from 'react';
+import axios from 'axios';
+import { useNavigate, Link } from 'react-router-dom';
+import Wrapper from '../assets/wrappers/RegisterAndLoginPage';
+import { Logo, FormRow } from '../components';
+import { Snackbar } from '@mui/material';
+
+const Login = () => {
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+ const [error, setError] = useState('');
+ const [openSnackbar, setOpenSnackbar] = useState(false);
+ const navigate = useNavigate();
+ const apiEndpoint =
+ process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000';
+
+ const login = async () => {
+ try {
+ await axios.post(`${apiEndpoint}/login`, { username, password });
+ setOpenSnackbar(true);
+ navigate('/dashboard');
+ } catch (error) {
+ setError(error.response.data.error);
+ }
+ };
+
+ const handleCloseSnackbar = () => {
+ setOpenSnackbar(false);
+ };
+
+ return (
+
+
+
+ {error && (
+ setError('')}
+ message={`Error: ${error}`}
+ />
+ )}
+
+ );
+};
+
+export default Login;
diff --git a/webapp/src/pages/Play.jsx b/webapp/src/pages/Play.jsx
new file mode 100644
index 0000000..640e13b
--- /dev/null
+++ b/webapp/src/pages/Play.jsx
@@ -0,0 +1,4 @@
+const Play = () => {
+ return Play Page ;
+};
+export default Play;
diff --git a/webapp/src/pages/Profile.jsx b/webapp/src/pages/Profile.jsx
new file mode 100644
index 0000000..6f4e2d0
--- /dev/null
+++ b/webapp/src/pages/Profile.jsx
@@ -0,0 +1,4 @@
+const Profile = () => {
+ return Profile Page ;
+};
+export default Profile;
diff --git a/webapp/src/pages/Ranking.jsx b/webapp/src/pages/Ranking.jsx
new file mode 100644
index 0000000..47ff145
--- /dev/null
+++ b/webapp/src/pages/Ranking.jsx
@@ -0,0 +1,4 @@
+const Ranking = () => {
+ return Ranking Page ;
+};
+export default Ranking;
diff --git a/webapp/src/pages/Stats.jsx b/webapp/src/pages/Stats.jsx
new file mode 100644
index 0000000..61524a0
--- /dev/null
+++ b/webapp/src/pages/Stats.jsx
@@ -0,0 +1,4 @@
+const Stats = () => {
+ return Stats Page ;
+};
+export default Stats;
diff --git a/webapp/src/pages/index.js b/webapp/src/pages/index.js
new file mode 100644
index 0000000..5115cf5
--- /dev/null
+++ b/webapp/src/pages/index.js
@@ -0,0 +1,12 @@
+export { default as Landing } from './Landing';
+export { default as DashboardLayout } from './DashboardLayout';
+export { default as HomeLayout } from './HomeLayout';
+export { default as AddUser } from './AddUser';
+export { default as Login } from './Login';
+export { default as Error } from './Error';
+export { default as Play } from './Play';
+export { default as Stats } from './Stats';
+export { default as Ranking } from './Ranking';
+export { default as EditJob } from './EditJob';
+export { default as Profile } from './Profile';
+export { default as Admin } from './Admin';
diff --git a/webapp/src/utils/links.jsx b/webapp/src/utils/links.jsx
new file mode 100644
index 0000000..4b42278
--- /dev/null
+++ b/webapp/src/utils/links.jsx
@@ -0,0 +1,37 @@
+import React from 'react';
+
+import { IoBarChartSharp } from 'react-icons/io5';
+import { MdQueryStats } from 'react-icons/md';
+import { BsQuestionSquare } from 'react-icons/bs';
+import { ImProfile } from 'react-icons/im';
+import { MdAdminPanelSettings } from 'react-icons/md';
+
+const links = [
+ {
+ text: 'play',
+ path: '.',
+ icon: ,
+ },
+ {
+ text: 'ranking',
+ path: 'ranking',
+ icon: ,
+ },
+ {
+ text: 'stats',
+ path: 'stats',
+ icon: ,
+ },
+ {
+ text: 'profile',
+ path: 'profile',
+ icon: ,
+ },
+ {
+ text: 'admin',
+ path: 'admin',
+ icon: ,
+ },
+];
+
+export default links;