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 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)} - /> - - - {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 ( +
+ + +
+ ); +}; + +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)} - /> - - - {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 wiq7; +}; + +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 ( + + +
+ +
+
+ ); +}; +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 ( + +
+ +

Register

+ setName(e.target.value)} + /> + setLastName(e.target.value)} + /> + setEmail(e.target.value)} + /> + setUsername(e.target.value)} + /> + setPassword(e.target.value)} + /> + +

+ Already a member? + + Login + +

+ + + {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 ( + +
+ not found +

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 + +
+ wiq 7 +
+
+ ); +}; + +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 ( + +
+ +

Login

+ setUsername(e.target.value)} + /> + setPassword(e.target.value)} + /> + + +

+ Not a member yet? + + Register + +

+ + + {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;