diff --git a/Backend/package-lock.json b/Backend/package-lock.json index 2f62807..1815168 100644 --- a/Backend/package-lock.json +++ b/Backend/package-lock.json @@ -10,6 +10,7 @@ "license": "GPL", "dependencies": { "@nestjs/common": "^10.0.0", + "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.0.0", "@nestjs/jwt": "^10.2.0", "@nestjs/platform-express": "^10.0.0", @@ -1783,6 +1784,29 @@ } } }, + "node_modules/@nestjs/config": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.1.1.tgz", + "integrity": "sha512-qu5QlNiJdqQtOsnB6lx4JCXPQ96jkKUsOGd+JXfXwqJqZcOSAq6heNFg0opW4pq4J/VZoNwoo87TNnx9wthnqQ==", + "dependencies": { + "dotenv": "16.3.1", + "dotenv-expand": "10.0.0", + "lodash": "4.17.21", + "uuid": "9.0.0" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "reflect-metadata": "^0.1.13" + } + }, + "node_modules/@nestjs/config/node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@nestjs/core": { "version": "10.2.7", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.2.7.tgz", @@ -4507,6 +4531,14 @@ "url": "https://github.com/motdotla/dotenv?sponsor=1" } }, + "node_modules/dotenv-expand": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", + "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", + "engines": { + "node": ">=12" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", diff --git a/Backend/package.json b/Backend/package.json index d91e4c2..666a4af 100644 --- a/Backend/package.json +++ b/Backend/package.json @@ -22,6 +22,7 @@ }, "dependencies": { "@nestjs/common": "^10.0.0", + "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.0.0", "@nestjs/jwt": "^10.2.0", "@nestjs/platform-express": "^10.0.0", diff --git a/Backend/src/app.module.ts b/Backend/src/app.module.ts index f75419c..dec5584 100644 --- a/Backend/src/app.module.ts +++ b/Backend/src/app.module.ts @@ -3,6 +3,7 @@ */ import { JwtModule } from '@nestjs/jwt'; +import { ConfigModule } from '@nestjs/config'; import { Module } from '@nestjs/common'; import { MachineLearningController } from './app/machineLearning/machineLearning.controller'; import { FrontendController } from './app/frontend/frontend.controller'; @@ -14,6 +15,7 @@ import { LoginService } from './login/login.service'; @Module({ imports: [ + ConfigModule.forRoot(), JwtModule.register({ global: true, secret: process.env.JWT_SECRET, diff --git a/Backend/src/database/database.service.ts b/Backend/src/database/database.service.ts index c5216fb..68df352 100644 --- a/Backend/src/database/database.service.ts +++ b/Backend/src/database/database.service.ts @@ -157,4 +157,4 @@ export class DatabaseService { return {}; } } -} +} \ No newline at end of file diff --git a/Backend/src/login/login.service.ts b/Backend/src/login/login.service.ts index 4a4e7f0..43d12df 100644 --- a/Backend/src/login/login.service.ts +++ b/Backend/src/login/login.service.ts @@ -31,4 +31,4 @@ export class LoginService { access_token: await this.jwtService.signAsync(user.name), }; } -} +} \ No newline at end of file diff --git a/Backend/src/main.ts b/Backend/src/main.ts index 2bde435..b934aba 100644 --- a/Backend/src/main.ts +++ b/Backend/src/main.ts @@ -34,6 +34,7 @@ async function bootstrap() { const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('swagger-api', app, document); + app.enableCors({ origin: true }); await app.listen(8080); console.log( diff --git a/Frontend/.env-example b/Frontend/.env-example new file mode 100644 index 0000000..96e8067 --- /dev/null +++ b/Frontend/.env-example @@ -0,0 +1 @@ +NEXT_PUBLIC_BACKEND_URL=http://localhost:8080/ \ No newline at end of file diff --git a/Frontend/package-lock.json b/Frontend/package-lock.json index 69b0f17..9377788 100644 --- a/Frontend/package-lock.json +++ b/Frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@ant-design/cssinjs": "^1.17.5", + "@hookform/resolvers": "^3.3.2", "@reduxjs/toolkit": "^1.9.7", "@types/react-redux": "^7.1.31", "antd": "^5.11.3", @@ -17,9 +18,12 @@ "next": "14.0.0", "react": "^18", "react-dom": "^18", + "react-hook-form": "^7.49.2", "react-icons": "^4.12.0", + "react-query": "^3.39.3", "react-redux": "^8.1.3", - "recharts": "^2.10.3" + "recharts": "^2.10.3", + "yup": "^1.3.3" }, "devDependencies": { "@types/node": "^20", @@ -204,6 +208,14 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@hookform/resolvers": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.2.tgz", + "integrity": "sha512-Tw+GGPnBp+5DOsSg4ek3LCPgkBOuOgS5DsDV7qsWNH9LZc433kgsWICjlsh2J9p04H2K66hsXPPb9qn9ILdUtA==", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", @@ -1253,8 +1265,15 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "engines": { + "node": ">=0.6" + } }, "node_modules/binary-extensions": { "version": "2.2.0", @@ -1269,7 +1288,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1287,6 +1305,21 @@ "node": ">=8" } }, + "node_modules/broadcast-channel": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz", + "integrity": "sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==", + "dependencies": { + "@babel/runtime": "^7.7.2", + "detect-node": "^2.1.0", + "js-sha3": "0.8.0", + "microseconds": "0.2.0", + "nano-time": "1.0.0", + "oblivious-set": "1.0.0", + "rimraf": "3.0.2", + "unload": "2.2.0" + } + }, "node_modules/browserslist": { "version": "4.22.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", @@ -1500,8 +1533,7 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/copy-to-clipboard": { "version": "3.3.3", @@ -1739,6 +1771,11 @@ "node": ">=6" } }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==" + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -2539,8 +2576,7 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { "version": "2.3.3", @@ -2639,7 +2675,6 @@ "version": "7.1.7", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -2881,7 +2916,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -2890,8 +2924,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/internal-slot": { "version": "1.0.6", @@ -3275,6 +3308,11 @@ "jiti": "bin/jiti.js" } }, + "node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3449,6 +3487,15 @@ "node": ">=10" } }, + "node_modules/match-sorter": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.1.tgz", + "integrity": "sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "remove-accents": "0.4.2" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -3471,6 +3518,11 @@ "node": ">=8.6" } }, + "node_modules/microseconds": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz", + "integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==" + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -3494,7 +3546,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -3528,6 +3579,14 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nano-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz", + "integrity": "sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA==", + "dependencies": { + "big-integer": "^1.6.16" + } + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -3773,11 +3832,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/oblivious-set": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz", + "integrity": "sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==" + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -3854,7 +3917,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -4083,6 +4145,11 @@ "react-is": "^16.13.1" } }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==" + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -4723,6 +4790,22 @@ "react": "^18.2.0" } }, + "node_modules/react-hook-form": { + "version": "7.49.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.49.2.tgz", + "integrity": "sha512-TZcnSc17+LPPVpMRIDNVITY6w20deMdNi6iehTFLV1x8SqThXGwu93HjlUVU09pzFgZH7qZOvLMM7UYf2ShAHA==", + "engines": { + "node": ">=18", + "pnpm": "8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-icons": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.12.0.tgz", @@ -4741,6 +4824,31 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "node_modules/react-query": { + "version": "3.39.3", + "resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.3.tgz", + "integrity": "sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "broadcast-channel": "^3.4.1", + "match-sorter": "^6.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-redux": { "version": "8.1.3", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz", @@ -4923,6 +5031,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/remove-accents": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz", + "integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==" + }, "node_modules/reselect": { "version": "4.1.8", "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", @@ -4982,7 +5095,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, "dependencies": { "glob": "^7.1.3" }, @@ -5445,6 +5557,11 @@ "node": ">=12.22" } }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==" + }, "node_modules/tiny-invariant": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz", @@ -5467,6 +5584,11 @@ "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + }, "node_modules/ts-api-utils": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", @@ -5625,6 +5747,15 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "dev": true }, + "node_modules/unload": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz", + "integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==", + "dependencies": { + "@babel/runtime": "^7.6.2", + "detect-node": "^2.0.4" + } + }, "node_modules/update-browserslist-db": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", @@ -5805,8 +5936,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/yallist": { "version": "4.0.0", @@ -5834,6 +5964,28 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yup": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.3.3.tgz", + "integrity": "sha512-v8QwZSsHH2K3/G9WSkp6mZKO+hugKT1EmnMqLNUcfu51HU9MDyhlETT/JgtzprnrnQHPWsjc6MUDMBp/l9fNnw==", + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } + }, + "node_modules/yup/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/Frontend/package.json b/Frontend/package.json index cb10f23..237b91a 100644 --- a/Frontend/package.json +++ b/Frontend/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@ant-design/cssinjs": "^1.17.5", + "@hookform/resolvers": "^3.3.2", "@reduxjs/toolkit": "^1.9.7", "@types/react-redux": "^7.1.31", "antd": "^5.11.3", @@ -18,9 +19,12 @@ "next": "14.0.0", "react": "^18", "react-dom": "^18", + "react-hook-form": "^7.49.2", "react-icons": "^4.12.0", + "react-query": "^3.39.3", "react-redux": "^8.1.3", - "recharts": "^2.10.3" + "recharts": "^2.10.3", + "yup": "^1.3.3" }, "devDependencies": { "@types/node": "^20", diff --git a/Frontend/src/api/authentication/login.tsx b/Frontend/src/api/authentication/login.tsx new file mode 100644 index 0000000..60608a5 --- /dev/null +++ b/Frontend/src/api/authentication/login.tsx @@ -0,0 +1,14 @@ +import { axiosClient } from "../axios-client"; +import { endpoints } from "../endpoints"; + +type LoginRequestDTO = { + name: string; + password: string; +}; + +type LoginResponseDTO = { + access_token: string; +}; + +export const login = (data: LoginRequestDTO) => + axiosClient.post(endpoints.login, data); diff --git a/Frontend/src/api/axios-client.ts b/Frontend/src/api/axios-client.ts new file mode 100644 index 0000000..b82a671 --- /dev/null +++ b/Frontend/src/api/axios-client.ts @@ -0,0 +1,19 @@ +import axios from "axios"; + +export const axiosClient = axios.create({ + baseURL: process.env.NEXT_PUBLIC_BACKEND_URL, + headers: { "X-Custom-Header": "foobar" }, +}); + +axiosClient.interceptors.response.use( + function (response) { + // Any status code that lie within the range of 2xx cause this function to trigger + // Do something with response data + return response; + }, + function (error) { + // Any status codes that falls outside the range of 2xx cause this function to trigger + // Do something with response error + return Promise.reject(error); + } +); diff --git a/Frontend/src/api/endpoints.ts b/Frontend/src/api/endpoints.ts new file mode 100644 index 0000000..8d91f5d --- /dev/null +++ b/Frontend/src/api/endpoints.ts @@ -0,0 +1,4 @@ +export const endpoints = { + /* authentication endpoints */ + login: "login", +}; diff --git a/Frontend/src/api/index.ts b/Frontend/src/api/index.ts new file mode 100644 index 0000000..c88e739 --- /dev/null +++ b/Frontend/src/api/index.ts @@ -0,0 +1,5 @@ +export { ReactQueryProvider } from "./react-query-provider"; +export { axiosClient } from "./axios-client"; + +/* authentication methods */ +export { login } from "./authentication/login"; diff --git a/Frontend/src/api/react-query-provider.tsx b/Frontend/src/api/react-query-provider.tsx new file mode 100644 index 0000000..caa93ea --- /dev/null +++ b/Frontend/src/api/react-query-provider.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { QueryClient, QueryClientProvider } from "react-query"; + +const queryClient = new QueryClient(); + +export const ReactQueryProvider = ({ + children, +}: { + children: React.ReactNode; +}) => ( + {children} +); diff --git a/Frontend/src/app/layout.tsx b/Frontend/src/app/layout.tsx index 8b47030..678e468 100644 --- a/Frontend/src/app/layout.tsx +++ b/Frontend/src/app/layout.tsx @@ -1,9 +1,12 @@ import type { Metadata } from "next"; import { Poppins } from "next/font/google"; import { StoreProvider } from "@/store"; +import { ReactQueryProvider } from "@/api"; import React from "react"; import { LayoutContainer } from "@/containers"; +import "@/app/globals.css"; + /* font */ const poppins = Poppins({ subsets: ["latin"], @@ -26,7 +29,9 @@ export default function RootLayout({ - {children} + + {children} + diff --git a/Frontend/src/app/login/page.tsx b/Frontend/src/app/login/page.tsx new file mode 100644 index 0000000..721e52c --- /dev/null +++ b/Frontend/src/app/login/page.tsx @@ -0,0 +1,5 @@ +import { LoginContainer } from "@/containers"; + +export default function LoginPage() { + return ; +} diff --git a/Frontend/src/app/recent-activities/page.tsx b/Frontend/src/app/recent-activities/page.tsx index 680a8be..5a308f7 100644 --- a/Frontend/src/app/recent-activities/page.tsx +++ b/Frontend/src/app/recent-activities/page.tsx @@ -9,6 +9,7 @@ export default function RecentActivities() { columns={recentActivitiesColumns} data={recentActivitiesData} pagination={{ total: recentActivitiesData.length }} + rowKey={"id"} /> ); diff --git a/Frontend/src/components/index.ts b/Frontend/src/components/index.ts index 4d72aad..49d0f45 100644 --- a/Frontend/src/components/index.ts +++ b/Frontend/src/components/index.ts @@ -2,3 +2,4 @@ export { VideoRecordingScreen } from "./video-recording-screen/video-recording-s export { Table } from "./table/table"; export { Button } from "./button/button"; export { Card } from "./card/card"; +export { Input } from "./input/input"; diff --git a/Frontend/src/components/input/input.tsx b/Frontend/src/components/input/input.tsx new file mode 100644 index 0000000..f51d6a0 --- /dev/null +++ b/Frontend/src/components/input/input.tsx @@ -0,0 +1,29 @@ +import { Input as AntInput } from "antd"; +import type { InputProps } from "antd"; + +type PropsType = { + label: string; + error?: string; + password?: boolean; +} & InputProps; + +/** This component renders a input field */ +export const Input: React.FC = ({ + label, + password = false, + error = undefined, + ...props +}) => { + return ( +
+
{label}
+ {password && ( + + )} + {!password && ( + + )} + {error &&

{error}

} +
+ ); +}; diff --git a/Frontend/src/components/table/table.tsx b/Frontend/src/components/table/table.tsx index 73bdab9..242e63e 100644 --- a/Frontend/src/components/table/table.tsx +++ b/Frontend/src/components/table/table.tsx @@ -1,4 +1,8 @@ -import type { ColumnsType, TablePaginationConfig } from "antd/es/table"; +import type { + ColumnsType, + TablePaginationConfig, + TableProps, +} from "antd/es/table"; import { Table as AntTable } from "antd"; import { AnyObject } from "@/types"; @@ -7,12 +11,18 @@ type PropsType = { columns: ColumnsType; pagination?: TablePaginationConfig; loading?: boolean; -}; +} & TableProps; /** This component renders a table */ export const Table: ( props: PropsType -) => React.ReactElement = ({ columns, data, loading = false, pagination }) => { +) => React.ReactElement = ({ + columns, + data, + loading = false, + pagination, + ...props +}) => { return (
( dataSource={data} loading={loading} pagination={pagination ? pagination : false} + {...props} />
); diff --git a/Frontend/src/containers/analytics-container/recent-authorized/recent-authorized.tsx b/Frontend/src/containers/analytics-container/recent-authorized/recent-authorized.tsx index f66c579..ba70503 100644 --- a/Frontend/src/containers/analytics-container/recent-authorized/recent-authorized.tsx +++ b/Frontend/src/containers/analytics-container/recent-authorized/recent-authorized.tsx @@ -11,7 +11,7 @@ export const RecentAuthorizedContainer: FC = () => { <>
- +
diff --git a/Frontend/src/containers/analytics-container/recent-unauthorized/recent-unauthorized.tsx b/Frontend/src/containers/analytics-container/recent-unauthorized/recent-unauthorized.tsx index 00e4592..e8d5b43 100644 --- a/Frontend/src/containers/analytics-container/recent-unauthorized/recent-unauthorized.tsx +++ b/Frontend/src/containers/analytics-container/recent-unauthorized/recent-unauthorized.tsx @@ -11,7 +11,7 @@ export const RecentUnauthorizedContainer: FC = () => { <>
-
+
diff --git a/Frontend/src/containers/index.ts b/Frontend/src/containers/index.ts index f790699..184436b 100644 --- a/Frontend/src/containers/index.ts +++ b/Frontend/src/containers/index.ts @@ -6,3 +6,4 @@ export { AboutUsSolutionContainer } from "./about-us-container/about-us-solution export { AboutUsTeamContainer } from "./about-us-container/about-us-team-container"; export { AboutUsMissionContainer } from "./about-us-container/about-us-mission-container"; export { AnalyticsContainer } from "./analytics-container/analytics-container"; +export { LoginContainer } from "./login-container"; diff --git a/Frontend/src/containers/layout-container.tsx b/Frontend/src/containers/layout-container.tsx index 6f6dd6b..6f5a70e 100644 --- a/Frontend/src/containers/layout-container.tsx +++ b/Frontend/src/containers/layout-container.tsx @@ -5,10 +5,11 @@ import { useRouter, usePathname } from "next/navigation"; import { loggedInNavBarItems, guestNavBarItems } from "@/data"; import { antTheme } from "../../theme"; import type { NavBarItem } from "@/types"; -import "@/app/globals.css"; import { getCurrentNav } from "@/utils"; import { BellOutlined } from "@ant-design/icons"; import { useSessionSlice, useCameraSlice } from "@/hooks"; +import { ProtectionContainer } from "./protection-container"; +import { NotificationContainer } from "./notification-container"; const { Header, Content, Footer, Sider } = Layout; export const LayoutContainer = ({ @@ -17,7 +18,7 @@ export const LayoutContainer = ({ children: React.ReactNode; }) => { /* state to check if ant design styled loaded */ - const { session } = useSessionSlice(); + const { session, logOut } = useSessionSlice(); const { isFullScreenGrid } = useCameraSlice(); const [antStyleLoaded, setAntStyleLoaded] = useState(false); const [currentNavMenu, setCurrentNavMenu] = useState([]); @@ -26,11 +27,12 @@ export const LayoutContainer = ({ /* event handler */ const onMenuClick = (info: any) => { - const selectedItem = loggedInNavBarItems.find( - (item) => item.key === info.key - ); - if (selectedItem) { + const selectedItem = currentNavMenu.find((item) => item.key === info.key); + if (selectedItem && selectedItem.key !== "logout") { router.push(selectedItem.route); + } else { + router.push("/login"); + logOut(); } }; @@ -38,59 +40,67 @@ export const LayoutContainer = ({ useEffect(() => { setAntStyleLoaded(true); }, []); + useEffect(() => { + /* navbar menu */ if (session) { setCurrentNavMenu(loggedInNavBarItems); } else { setCurrentNavMenu(guestNavBarItems); } - }, [session]); + }, [session, pathname]); return ( - {antStyleLoaded && ( - - -
- logo -
+ + {antStyleLoaded && ( + + + +
+ logo +
- ({ - key: item.key, - icon: React.createElement(item.icon), - label: item.label, - }))} - onClick={onMenuClick} - /> - - -
- {session && ( - - )} -
- -
{children}
-
-
- CSS ©2023 Created by CSS team -
-
- - )} + ({ + key: item.key, + icon: React.createElement(item.icon), + label: item.label, + }) + )} + onClick={onMenuClick} + /> + + +
+ {session && ( + + )} +
+ +
{children}
+
+
+ CSS ©2023 Created by CSS team +
+
+ + + )} + ); }; diff --git a/Frontend/src/containers/login-container.tsx b/Frontend/src/containers/login-container.tsx new file mode 100644 index 0000000..038f189 --- /dev/null +++ b/Frontend/src/containers/login-container.tsx @@ -0,0 +1,82 @@ +"use client"; +import { useState } from "react"; +import { useForm, Controller } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import type { FC } from "react"; +import React from "react"; +import { Input, Button } from "@/components"; +import { loginFormSchema } from "@/data"; +import { login } from "@/api"; +import { useNotificationSlice, useSessionSlice } from "@/hooks"; +import { isEmptyObject } from "@/utils"; + +/* This container renders login sections */ +export const LoginContainer: FC = () => { + /* state*/ + const [isLoading, setIsLoading] = useState(false); + const { + control, + handleSubmit, + formState: { errors, isValid }, + } = useForm({ + resolver: yupResolver(loginFormSchema), + }); + const { openNotification } = useNotificationSlice(); + const { logIn } = useSessionSlice(); + + /* event handlers */ + const onSubmit = async (data: any) => { + try { + setIsLoading(true); + const result = await login(data); + logIn({ + accessToken: result.data.access_token, + user: { + firstName: "Nabil Mohammed", + lastName: "Khelifa", + email: "nabil.nablotech@gmail.com", + mobileNumber: "393513117160", + }, + }); + } catch (error: any) { + openNotification({ type: "error", message: error.response.data.message }); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
CSS
+
Login into your account
+
+ ( + + )} + /> + + ( + + )} + /> + +
+ +
+ +
+ ); +}; diff --git a/Frontend/src/containers/notification-container.tsx b/Frontend/src/containers/notification-container.tsx new file mode 100644 index 0000000..d529f97 --- /dev/null +++ b/Frontend/src/containers/notification-container.tsx @@ -0,0 +1,31 @@ +import { useNotificationSlice } from "@/hooks"; +import { notification as antNotification } from "antd"; +import { useEffect } from "react"; + +export const NotificationContainer = ({ + children, +}: { + children: React.ReactNode; +}) => { + const [api, contextHolder] = antNotification.useNotification(); + + const { notification } = useNotificationSlice(); + + /* useEffect */ + useEffect(() => { + if (notification) { + api[notification.type]({ + message: notification.type.toUpperCase(), + description: notification.message, + placement: "bottomRight", + }); + } + }, [notification]); + + return ( + <> + {notification && <>{contextHolder}} + {children} + + ); +}; diff --git a/Frontend/src/containers/protection-container.tsx b/Frontend/src/containers/protection-container.tsx new file mode 100644 index 0000000..76c3562 --- /dev/null +++ b/Frontend/src/containers/protection-container.tsx @@ -0,0 +1,50 @@ +"use client"; +import React, { useEffect, useState } from "react"; +import { useRouter, usePathname } from "next/navigation"; +import { loggedInNavBarItems, guestNavBarItems } from "@/data"; +import { useSessionSlice } from "@/hooks"; + +export const ProtectionContainer = ({ + children, +}: { + children: React.ReactNode; +}) => { + /* state to check if ant design styled loaded */ + const { session } = useSessionSlice(); + const router = useRouter(); + const pathname = usePathname(); + const [isAllowedPath, setIsAllowedPath] = useState(); + const [renderChildren, setRenderChildren] = useState(false); + + /* useEffect */ + useEffect(() => { + if ( + (loggedInNavBarItems.find((navItem) => pathname === navItem.route) && + session) || + guestNavBarItems.find((navItem) => pathname === navItem.route) + ) { + setIsAllowedPath(true); + } else { + setIsAllowedPath(false); + } + }, [session, pathname]); + + useEffect(() => { + if (isAllowedPath !== undefined) { + if (!isAllowedPath) { + router.push("/login"); + } else if ( + isAllowedPath && + session && + (pathname === "/login" || pathname === "/") + ) { + setRenderChildren(false); + router.push("/video-stream"); + } else if (isAllowedPath) { + setRenderChildren(true); + } + } + }, [isAllowedPath, pathname, session]); + + return <>{renderChildren && <>{children}}; +}; diff --git a/Frontend/src/data/form-schema-data.tsx b/Frontend/src/data/form-schema-data.tsx new file mode 100644 index 0000000..95af96f --- /dev/null +++ b/Frontend/src/data/form-schema-data.tsx @@ -0,0 +1,8 @@ +import * as yup from "yup"; + +export const loginFormSchema = yup + .object({ + name: yup.string().required("This field is required"), + password: yup.string().required("This field is required"), + }) + .required(); diff --git a/Frontend/src/data/index.ts b/Frontend/src/data/index.ts index c5960e9..582d214 100644 --- a/Frontend/src/data/index.ts +++ b/Frontend/src/data/index.ts @@ -10,3 +10,4 @@ export { recentActivitiesData, recentActivitiesColumns, } from "./recent-activities-data"; +export { loginFormSchema } from "./form-schema-data"; diff --git a/Frontend/src/hooks/index.ts b/Frontend/src/hooks/index.ts index 43222db..6466cf4 100644 --- a/Frontend/src/hooks/index.ts +++ b/Frontend/src/hooks/index.ts @@ -1,3 +1,4 @@ export { useAppDispatch, useAppSelector } from "./store-hooks"; export { useCameraSlice } from "./use-camera-slice"; export { useSessionSlice } from "./use-session-slice"; +export { useNotificationSlice } from "./use-notification-slice"; diff --git a/Frontend/src/hooks/use-notification-slice.ts b/Frontend/src/hooks/use-notification-slice.ts new file mode 100644 index 0000000..f872074 --- /dev/null +++ b/Frontend/src/hooks/use-notification-slice.ts @@ -0,0 +1,31 @@ +import { useAppDispatch, useAppSelector } from "./store-hooks"; +import { Notification } from "@/types"; +import { + selectNotification, + openNotification, + closeNotification, +} from "@/store"; + +export const useNotificationSlice = () => { + const dispatch = useAppDispatch(); + + /* redux notification state properties */ + const notification: Notification = useAppSelector(selectNotification); + + /* redux notification state updaters */ + const openNotificationState = ( + newNotification: Exclude + ) => { + dispatch(openNotification(newNotification)); + }; + + const closeNotificationState = () => { + dispatch(closeNotification()); + }; + + return { + notification: notification, + openNotification: openNotificationState, + closeNotification: closeNotificationState, + }; +}; diff --git a/Frontend/src/hooks/use-session-slice.ts b/Frontend/src/hooks/use-session-slice.ts index ede20ed..532f58f 100644 --- a/Frontend/src/hooks/use-session-slice.ts +++ b/Frontend/src/hooks/use-session-slice.ts @@ -1,6 +1,6 @@ import { useAppDispatch, useAppSelector } from "./store-hooks"; import { Session } from "@/types"; -import { selectSession, logIn } from "@/store"; +import { selectSession, logIn, logOut } from "@/store"; export const useSessionSlice = () => { const dispatch = useAppDispatch(); @@ -9,12 +9,17 @@ export const useSessionSlice = () => { const session: Session = useAppSelector(selectSession); /* redux session state updaters */ - const logInState = (newSession: Exclude) => { + const logInState = (newSession: Session) => { dispatch(logIn(newSession)); }; + const logOutState = () => { + dispatch(logOut()); + }; + return { session: session, logIn: logInState, + logOut: logOutState, }; }; diff --git a/Frontend/src/store/index.ts b/Frontend/src/store/index.ts index abd73ef..63aaae5 100644 --- a/Frontend/src/store/index.ts +++ b/Frontend/src/store/index.ts @@ -1,6 +1,6 @@ export type { RootState, AppDispatch } from "./store"; export { StoreProvider } from "./store-provider"; -export { selectSession, logIn } from "./slices/session-slice"; +export { selectSession, logIn, logOut } from "./slices/session-slice"; export { selectCameras, selectCameraCount, @@ -9,3 +9,8 @@ export { updateCamera, toggleIsFullScreenGrid, } from "./slices/camera-slice"; +export { + selectNotification, + openNotification, + closeNotification, +} from "./slices/notification-slice"; diff --git a/Frontend/src/store/slices/notification-slice.ts b/Frontend/src/store/slices/notification-slice.ts new file mode 100644 index 0000000..cc0f8b2 --- /dev/null +++ b/Frontend/src/store/slices/notification-slice.ts @@ -0,0 +1,34 @@ +import { createSlice } from "@reduxjs/toolkit"; +import type { PayloadAction } from "@reduxjs/toolkit"; +import type { RootState } from "../store"; +import { Notification, NotificationState } from "@/types"; + +// Define the initial state using that type +const initialState = { currentNotification: null } as NotificationState; + +export const notificationSlice = createSlice({ + name: "notification", + // `createSlice` will infer the state type from the `initialState` argument + initialState, + reducers: { + // Use the PayloadAction type to declare the contents of `action.payload` + openNotification: ( + state: NotificationState, + action: PayloadAction + ) => { + state.currentNotification = action.payload; + }, + closeNotification: (state: NotificationState) => { + state.currentNotification = null; + }, + }, +}); + +export const { openNotification, closeNotification } = + notificationSlice.actions; + +// Other code such as selectors can use the imported `RootState` type +export const selectNotification = (state: RootState) => + state.notification.currentNotification; + +export default notificationSlice.reducer; diff --git a/Frontend/src/store/slices/session-slice.ts b/Frontend/src/store/slices/session-slice.ts index dbb35b0..c004884 100644 --- a/Frontend/src/store/slices/session-slice.ts +++ b/Frontend/src/store/slices/session-slice.ts @@ -1,18 +1,12 @@ import { createSlice } from "@reduxjs/toolkit"; import type { PayloadAction } from "@reduxjs/toolkit"; import type { RootState } from "../store"; -import { Session } from "@/types"; +import { Session, SessionState } from "@/types"; // Define the initial state using that type const initialState = { - accessToken: "asdf1234hbhj123bkdshfh2389317492013r0hf1273y4rwefh29fy10", - user: { - firstName: "Nabil Mohammed", - lastName: "Khelifa", - email: "nabil.nablotech@gmail.com", - mobileNumber: "393513117160", - }, -} as Session; + currentSession: null, +} as SessionState; export const sessionSlice = createSlice({ name: "session", @@ -20,15 +14,18 @@ export const sessionSlice = createSlice({ initialState, reducers: { // Use the PayloadAction type to declare the contents of `action.payload` - logIn: (state: Session, action: PayloadAction>) => { - state = action.payload; + logIn: (state: SessionState, action: PayloadAction) => { + state.currentSession = action.payload; + }, + logOut: (state: SessionState) => { + state.currentSession = null; }, }, }); -export const { logIn } = sessionSlice.actions; +export const { logIn, logOut } = sessionSlice.actions; // Other code such as selectors can use the imported `RootState` type -export const selectSession = (state: RootState) => state.session; +export const selectSession = (state: RootState) => state.session.currentSession; export default sessionSlice.reducer; diff --git a/Frontend/src/store/store.ts b/Frontend/src/store/store.ts index 082ff76..a34849e 100644 --- a/Frontend/src/store/store.ts +++ b/Frontend/src/store/store.ts @@ -1,11 +1,13 @@ import { configureStore } from "@reduxjs/toolkit"; import sessionReducer from "./slices/session-slice"; import cameraReducer from "./slices/camera-slice"; +import notificationReducer from "./slices/notification-slice"; export const store = configureStore({ reducer: { session: sessionReducer, camera: cameraReducer, + notification: notificationReducer, }, }); diff --git a/Frontend/src/types/index.ts b/Frontend/src/types/index.ts index 7a60f95..daabbf6 100644 --- a/Frontend/src/types/index.ts +++ b/Frontend/src/types/index.ts @@ -4,7 +4,11 @@ export type { Session } from "./models/session"; export type { Camera } from "./models/camera"; export type { AuthorizedEntity } from "./models/authorized-entity"; export type { Activity } from "./models/activity"; +export type { Notification } from "./models/notification"; /* utils */ export type { ColorVariant } from "./utils/color-variant"; export type { NavBarItem } from "./utils/navbar-item"; export type { AnyObject } from "./utils/any-object"; +/* redux */ +export type { SessionState } from "./redux-state/session-state"; +export type { NotificationState } from "./redux-state/notification-state"; diff --git a/Frontend/src/types/models/notification.ts b/Frontend/src/types/models/notification.ts new file mode 100644 index 0000000..8b6e5a6 --- /dev/null +++ b/Frontend/src/types/models/notification.ts @@ -0,0 +1,9 @@ + +/** This is a data type that denotes a notification. notification is composed of + * properties such as type, message, etc. + * It is null if there is no notification. + * */ +export type Notification = { + type: "info" | "error" | "success"; + message: string; +} | null; diff --git a/Frontend/src/types/models/session.ts b/Frontend/src/types/models/session.ts index 63f71a2..1425d4f 100644 --- a/Frontend/src/types/models/session.ts +++ b/Frontend/src/types/models/session.ts @@ -2,10 +2,8 @@ import { User } from "./user"; /** This is a data type that denotes a session. Session is composed of * properties required when a user is logged in such as accessToken,user, etc. - * It is null when the user is not logged in. * */ export type Session = { accessToken: string; user: User; } | null; - diff --git a/Frontend/src/types/redux-state/notification-state.ts b/Frontend/src/types/redux-state/notification-state.ts new file mode 100644 index 0000000..4dd77f5 --- /dev/null +++ b/Frontend/src/types/redux-state/notification-state.ts @@ -0,0 +1,7 @@ +import { Notification } from "../models/notification"; + +/** This is a data type that denotes a data type of the Notification redux slice. + * */ +export type NotificationState = { + currentNotification: Notification | null; +}; diff --git a/Frontend/src/types/redux-state/session-state.ts b/Frontend/src/types/redux-state/session-state.ts new file mode 100644 index 0000000..cfa0021 --- /dev/null +++ b/Frontend/src/types/redux-state/session-state.ts @@ -0,0 +1,7 @@ +import { Session } from "../models/session"; + +/** This is a data type that denotes a data type of the session redux slice. + * */ +export type SessionState = { + currentSession: Session; +}; diff --git a/Frontend/src/utils/index.ts b/Frontend/src/utils/index.ts index 44510c8..9a6a10f 100644 --- a/Frontend/src/utils/index.ts +++ b/Frontend/src/utils/index.ts @@ -1,3 +1,4 @@ export { getCurrentNav } from "./get-current-nav"; export { getItemObject } from "./get-item-object"; export { getObjectArray } from "./get-object-array"; +export { isEmptyObject } from "./is-empty-object"; diff --git a/Frontend/src/utils/is-empty-object.ts b/Frontend/src/utils/is-empty-object.ts new file mode 100644 index 0000000..bf5f4e7 --- /dev/null +++ b/Frontend/src/utils/is-empty-object.ts @@ -0,0 +1,3 @@ +export const isEmptyObject = (obj: Record): boolean => { + return Object.keys(obj).length === 0; +}; diff --git a/Frontend/tsconfig.json b/Frontend/tsconfig.json index 25f2d1e..299c000 100644 --- a/Frontend/tsconfig.json +++ b/Frontend/tsconfig.json @@ -25,6 +25,7 @@ "@/data": ["./src/data"], "@/hooks": ["./src/hooks"], "@/store": ["./src/store"], + "@/api": ["./src/api"], "@/types": ["./src/types"], "@/utils": ["./src/utils"] } diff --git a/README.md b/README.md index 8f50b16..fca11d6 100644 --- a/README.md +++ b/README.md @@ -30,3 +30,43 @@ Then you can run ```bash $ docker compose up -d ``` + +# Frontend + +This is the frontend part of the camera security system. It is this part that enables +user to interact with the system visually. + +### 0- Prerequisite +You should first install node.js in your machine. +You can get more information [here](https://nodejs.org/en/learn/getting-started/how-to-install-nodejs) +### 1- Open terminal + +First open the terminal and move to the frontend directory. + +If you are currently in root directory of this repo. Type the following in the terminal to change directory: +```yaml +cd ./Frontend/ +``` + +### 2- Define environment variables +In order to run the application you will need to define the environment variable by creating +.env file in the root Frontend directory. + +Copy the content of .env_example found in the Frontend root directory and change the variable values +accordingly. + +### 3- Install packages + +Install the required packages by running: +```yaml +npm install +``` + +### 4- Run the application + +Run the application using this command: + +```yaml +npm run dev +``` +Note: If you are running the backend locally make sure that you followed the steps in the [backend section](#backend).