diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..9f1f131d6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vite \ No newline at end of file diff --git a/Client/package-lock.json b/Client/package-lock.json index 2ad129bd4..fe3d684f5 100644 --- a/Client/package-lock.json +++ b/Client/package-lock.json @@ -527,358 +527,6 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild/win32-x64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", @@ -975,9 +623,9 @@ } }, "node_modules/@floating-ui/dom": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.4.tgz", - "integrity": "sha512-0G8R+zOvQsAG1pg2Q99P21jiqxqGBW1iRe/iXHsBRBxnpXKFI8QwbB4x5KmYLggNO5m34IQgOIu9SCRfR/WWiQ==", + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.5.tgz", + "integrity": "sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==", "dependencies": { "@floating-ui/core": "^1.0.0", "@floating-ui/utils": "^0.2.0" @@ -1476,208 +1124,13 @@ } }, "node_modules/@remix-run/router": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.0.tgz", - "integrity": "sha512-Quz1KOffeEf/zwkCBM3kBtH4ZoZ+pT3xIXBG4PPW/XFtDP7EGhtTiC2+gpL9GnR7+Qdet5Oa6cYSvwKYg6kN9Q==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", + "integrity": "sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==", "engines": { "node": ">=14.0.0" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz", - "integrity": "sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz", - "integrity": "sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz", - "integrity": "sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz", - "integrity": "sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz", - "integrity": "sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.17.2.tgz", - "integrity": "sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz", - "integrity": "sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz", - "integrity": "sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.17.2.tgz", - "integrity": "sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz", - "integrity": "sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.17.2.tgz", - "integrity": "sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz", - "integrity": "sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz", - "integrity": "sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz", - "integrity": "sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz", - "integrity": "sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@rollup/rollup-win32-x64-msvc": { "version": "4.17.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz", @@ -2981,20 +2434,6 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -4186,9 +3625,9 @@ ] }, "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", "dependencies": { "loose-envify": "^1.1.0" }, @@ -4197,15 +3636,15 @@ } }, "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", "dependencies": { "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" + "scheduler": "^0.23.0" }, "peerDependencies": { - "react": "^18.3.1" + "react": "^18.2.0" } }, "node_modules/react-is": { @@ -4223,11 +3662,11 @@ } }, "node_modules/react-router": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.0.tgz", - "integrity": "sha512-wPMZ8S2TuPadH0sF5irFGjkNLIcRvOSaEe7v+JER8508dyJumm6XZB1u5kztlX0RVq6AzRVndzqcUh6sFIauzA==", + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.1.tgz", + "integrity": "sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==", "dependencies": { - "@remix-run/router": "1.16.0" + "@remix-run/router": "1.16.1" }, "engines": { "node": ">=14.0.0" @@ -4237,12 +3676,12 @@ } }, "node_modules/react-router-dom": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.0.tgz", - "integrity": "sha512-Q9YaSYvubwgbal2c9DJKfx6hTNoBp3iJDsl+Duva/DwxoJH+OTXkxGpql4iUK2sla/8z4RpjAm6EWx1qUDuopQ==", + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.1.tgz", + "integrity": "sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==", "dependencies": { - "@remix-run/router": "1.16.0", - "react-router": "6.23.0" + "@remix-run/router": "1.16.1", + "react-router": "6.23.1" }, "engines": { "node": ">=14.0.0" diff --git a/Client/src/App.jsx b/Client/src/App.jsx index b47c3082e..94ef75111 100644 --- a/Client/src/App.jsx +++ b/Client/src/App.jsx @@ -5,6 +5,7 @@ import Login from "./Pages/Login"; import Register from "./Pages/Register"; import HomeLayout from "./Layouts/HomeLayout"; import Demo from "./Pages/Demo/Demo"; +import PlayGround from "./Pages/PlayGround/PlayGround"; function App() { return ( @@ -15,6 +16,7 @@ function App() { } /> } /> } /> + } /> ); diff --git a/Client/src/Components/Announcements/AnnouncementUpdateSubscription/AnnouncementUpdateSubscription.jsx b/Client/src/Components/Announcements/AnnouncementUpdateSubscription/AnnouncementUpdateSubscription.jsx new file mode 100644 index 000000000..f3363e866 --- /dev/null +++ b/Client/src/Components/Announcements/AnnouncementUpdateSubscription/AnnouncementUpdateSubscription.jsx @@ -0,0 +1,71 @@ +import "./announcementUpdateSubscription.css"; +import React from "react"; +import PropTypes from "prop-types"; +import { useTheme } from "@mui/material"; + +/** + * @component + * @param {Object} props + * @param {string} props.title - Title text for the update subscription dialog (required) + * @param {string} props.text - Body text for the update subscription dialog (required) + * @param {string} props.cancel - Text for the negative (cancel) button (required) + * @param {string} props.positive - Text for the positive button (required) + * @param {string} props.header - Title text for the email subscription section (required) + * @param {string} props.button - Text for the subscribe button (required) + * @returns {JSX.Element} - Renders the announcement update subscription component + */ + +const AnnouncementUpdateSubscription = ({ + title, + text, + cancel, + positive, + header, + button, +}) => { + const theme = useTheme(); + + const fontLookup = { + default: theme.font.default.font, + }; + + const fontFamily = fontLookup["default"]; + + return ( +
+
+
{title}
+
+
{text}
+
+
+ + +
+
+
+
+
{header}
+
+ + +
+
+
+ ); +}; + +AnnouncementUpdateSubscription.propTypes = { + title: PropTypes.string.isRequired, + text: PropTypes.string.isRequired, + cancel: PropTypes.string.isRequired, + positive: PropTypes.string.isRequired, + header: PropTypes.string.isRequired, + button: PropTypes.string.isRequired, +}; + +export default AnnouncementUpdateSubscription; diff --git a/Client/src/Components/Announcements/AnnouncementUpdateSubscription/announcementUpdateSubscription.css b/Client/src/Components/Announcements/AnnouncementUpdateSubscription/announcementUpdateSubscription.css new file mode 100644 index 000000000..390a130d4 --- /dev/null +++ b/Client/src/Components/Announcements/AnnouncementUpdateSubscription/announcementUpdateSubscription.css @@ -0,0 +1,103 @@ +:root { + --update-subscription-color-0: #101828; + --update-subscription-color-1: #475467; + --update-subscription-color-2: #fff; + --update-subscription-color-3: #eaecf0; + --update-subscription-color-4: #7f56d9; + --update-subscription-color-5: #1570ef; + --update-subscription-color-6: #344054; + --update-subscription-color-7: #d0d5dd; + --update-subscription-padding-0: 12px 16px; + --update-subscription-margin-right: 12px; + --update-subscription-font-size: 13px; + --update-subscription-radius-0: 4px; + --spacing-1: 16px; + --spacing-2: 20px; +} + +.update-subscription { + margin: var(--spacing-2); +} + +.update-subscription, +.update-subscription-dialog { + max-width: 770px; + border-radius: 4px; + border: 1px solid #eaecf0; +} + +.update-subscription-dialog { + padding: 15px 30px; +} + +.subscription-dialog-title { + font-size: 1rem; + font-weight: 700; + color: var(--update-subscription-color-0); +} + +.v-spacing-small { + height: 10px; +} + +.v-spacing-medium { + height: 15px; +} + +.subscription-dialog-text { + font-size: 13px; + color: var(--update-subscription-color-1); +} + +.subscription-dialog-controls { + display: flex; +} + +.controls-negative { + padding: var(--update-subscription-padding-0); + margin-right: var(--update-subscription-margin-right); + border-radius: var(--update-subscription-radius-0); + border: 1px solid var(--update-subscription-color-3); + background-color: var(--update-subscription-color-2); + font-size: var(--update-subscription-font-size); +} + +.controls-positive, +.subscribe-button { + padding: var(--update-subscription-padding-0); + margin-right: var(--update-subscription-margin-right); + border-radius: var(--update-subscription-radius-0); + border: 1px solid var(--update-subscription-color-4); + background-color: var(--update-subscription-color-5); + color: var(--update-subscription-color-2); + font-size: var(--update-subscription-font-size); +} + +.update-subscription-title { + color: var(--update-subscription-color-6); + padding: 0 var(--spacing-2); + font-weight: 700; + font-size: 13px; + margin-bottom: 10px; +} + +.subscription-email { + display: flex; + padding: 0 var(--spacing-2); +} + +.subscription-email-field { + width: 290px; + height: 24px; + border-radius: var(--update-subscription-radius-0); + border: 1px solid var(--update-subscription-color-7); + box-shadow: 0px 1px 2px 0px rgba(101, 182, 28, 0.05); + padding: 5px 15px; + margin-right: var(--spacing-2); +} + +.subscribe-button { + width: 100px; + height: 34px; + padding: 2px 15px; +} diff --git a/Client/src/Components/Announcements/AnnouncementsDualButton/AnnouncementsDualButton.jsx b/Client/src/Components/Announcements/AnnouncementsDualButton/AnnouncementsDualButton.jsx new file mode 100644 index 000000000..2abbadb1e --- /dev/null +++ b/Client/src/Components/Announcements/AnnouncementsDualButton/AnnouncementsDualButton.jsx @@ -0,0 +1,55 @@ +import "./announcementsDualButton.css"; +import React from "react"; +import CloseIcon from "@mui/icons-material/Close"; +import PropTypes from "prop-types"; +import { useTheme } from "@mui/material"; + +/** + * @component + * @param {Object} props + * @param {string} props.subject - The announcement subject text + * @param {string} props.body - The announcement body text content + * @param {string} props.esc - The text for the escape button + * @param {string} props.primary - The text for the primary button + * @returns {JSX.Element} - Renders the announcement dual button component + */ + +const AnnouncementsDualButton = ({ subject, body, esc, primary }) => { + const theme = useTheme(); + + const fontLookup = { + default: theme.font.default.font, + }; + + const fontFamily = fontLookup["default"]; + + return ( +
+
+
{subject}
+
+
{body}
+
+
{esc}
+
{primary}
+
+
+
+
+ +
+
+ ); +}; + +AnnouncementsDualButton.propTypes = { + subject: PropTypes.string.isRequired, + body: PropTypes.string.isRequired, + esc: PropTypes.string.isRequired, + primary: PropTypes.string.isRequired, +}; + +export default AnnouncementsDualButton; diff --git a/Client/src/Components/Announcements/AnnouncementsDualButton/announcementsDualButton.css b/Client/src/Components/Announcements/AnnouncementsDualButton/announcementsDualButton.css new file mode 100644 index 000000000..92fbe352a --- /dev/null +++ b/Client/src/Components/Announcements/AnnouncementsDualButton/announcementsDualButton.css @@ -0,0 +1,38 @@ +:root { + --announcement-border-color: #d0d5dd; + --announcement-border-radius: 4px; + --announcement-border-radius-2: 8px; + --announcement-font-size-0: 13px; + --announcement-font-color-0: #344054; + --announcement-font-color-1: #475467; + --announcement-font-color-2: #1570ef; + --announcement-padding: 16px; + --info-icon-frame-color: #eaecf0; + --margin: 20px; +} + +.announcement-without-tile { + margin: var(--margin); + max-width: 343px; + width: fit-content; + display: flex; + border: 1px solid var(--announcement-border-color); + padding: var(--announcement-padding); + border-radius: var(--announcement-border-radius); +} + +.h-spacing { + width: 20px; +} + +.announcement-without-content-subject { + font-size: var(--announcement-font-size-0); + color: var(--announcement-font-color-0); + font-weight: 700; + margin-bottom: 10px; +} + +.announcement-without-content-body { + font-size: var(--announcement-font-size-0); + color: var(--announcement-font-color-1); +} diff --git a/Client/src/Components/Announcements/AnnouncementsDualButtonWithIcon/AnnouncementsDualButtonWithIcon.jsx b/Client/src/Components/Announcements/AnnouncementsDualButtonWithIcon/AnnouncementsDualButtonWithIcon.jsx new file mode 100644 index 000000000..574c255a0 --- /dev/null +++ b/Client/src/Components/Announcements/AnnouncementsDualButtonWithIcon/AnnouncementsDualButtonWithIcon.jsx @@ -0,0 +1,65 @@ +import "./announcementsDualButtonWithIcon.css"; +import React from "react"; +import CloseIcon from "@mui/icons-material/Close"; +import PropTypes from "prop-types"; +import { useTheme } from "@mui/material"; + +/** + * @component + * @param {Object} props + * @param {React.ReactNode} props.icon - Optional React node for an announcement icon + * @param {string} props.subject - The announcement subject text + * @param {string} props.body - The announcement body text content + * @param {string} props.esc - The text for the escape button (usually "Close") + * @param {string} props.primary - The text for the primary button + * @returns {JSX.Element} - Renders the announcement dual button with icon component + */ + +const AnnouncementsDualButtonWithIcon = ({ + icon, + subject, + body, + esc, + primary, +}) => { + const theme = useTheme(); + + const fontLookup = { + default: theme.font.default.font, + }; + + const fontFamily = fontLookup["default"]; + + return ( +
+ {icon &&
{icon}
} +
+ {subject && ( +
{subject}
+ )} + {body &&
{body}
} + {(esc || primary) && ( +
+ {esc &&
{esc}
} + {primary && ( +
{primary}
+ )} +
+ )} +
+
+ +
+
+ ); +}; + +AnnouncementsDualButtonWithIcon.propTypes = { + icon: PropTypes.object, + subject: PropTypes.string, + body: PropTypes.string, + esc: PropTypes.string, + primary: PropTypes.string, +}; + +export default AnnouncementsDualButtonWithIcon; diff --git a/Client/src/Components/Announcements/AnnouncementsDualButtonWithIcon/announcementsDualButtonWithIcon.css b/Client/src/Components/Announcements/AnnouncementsDualButtonWithIcon/announcementsDualButtonWithIcon.css new file mode 100644 index 000000000..1f1659060 --- /dev/null +++ b/Client/src/Components/Announcements/AnnouncementsDualButtonWithIcon/announcementsDualButtonWithIcon.css @@ -0,0 +1,71 @@ +:root { + --announcement-border-color: #d0d5dd; + --announcement-border-radius: 4px; + --announcement-border-radius-2: 8px; + --announcement-font-size-0: 13px; + --announcement-font-color-0: #344054; + --announcement-font-color-1: #475467; + --announcement-font-color-2: #1570ef; + --announcement-padding: 16px; + --info-icon-frame-color: #eaecf0; + --tile-margin: 20px; +} + +.announcement-tile { + margin: var(--tile-margin); + width: fit-content; + display: flex; + border: 1px solid var(--announcement-border-color); + padding: var(--announcement-padding); + border-radius: var(--announcement-border-radius); +} + +.info-icon-frame { + display: flex; + width: 40px; + height: 40px; + border: 1px solid var(--info-icon-frame-color); + border-radius: var(--announcement-border-radius-2); + justify-content: center; + align-items: center; +} + +.announcement-content { + padding: 0 var(--announcement-padding); +} + +.announcement-content-subject { + font-size: var(--announcement-font-size-0); + color: var(--announcement-font-color-0); + font-weight: 700; + margin-bottom: 10px; +} + +.announcement-content-body { + font-size: var(--announcement-font-size-0); + color: var(--announcement-font-color-1); + font-weight: 300; +} + +.announcement-content-controllers { + display: flex; + font-size: var(--announcement-font-size-0); + margin-top: 15px; +} + +.controllers-button-esc { + font-size: var(--announcement-font-size-0); + font-weight: 600; + margin-right: 12px; +} + +.controllers-button-primary { + font-size: var(--announcement-font-size-0); + font-weight: 600; + color: var(--announcement-font-color-2); +} + +.announcement-close { + width: 24px; + height: 24px; +} diff --git a/Client/src/Components/Announcements/AnnouncementsMessageBar/AnnouncementsMessageBar.jsx b/Client/src/Components/Announcements/AnnouncementsMessageBar/AnnouncementsMessageBar.jsx new file mode 100644 index 000000000..20a179052 --- /dev/null +++ b/Client/src/Components/Announcements/AnnouncementsMessageBar/AnnouncementsMessageBar.jsx @@ -0,0 +1,36 @@ +import "./announcementsMessageBar.css"; +import React from "react"; +import CloseIcon from "@mui/icons-material/Close"; +import PropTypes from "prop-types"; +import { useTheme } from "@mui/material"; + +/** + * @component + * @param {Object} props + * @param {string} props.message - The announcement message text content (required) + * @returns {JSX.Element} - Renders the announcement message bar component + */ +const AnnouncementsMessageBar = ({ message }) => { + const theme = useTheme(); + + const fontLookup = { + default: theme.font.default.font, + }; + + const fontFamily = fontLookup["default"]; + + return ( +
+
{message}
+
+ +
+
+ ); +}; + +AnnouncementsMessageBar.propTypes = { + message: PropTypes.string.isRequired, +}; + +export default AnnouncementsMessageBar; diff --git a/Client/src/Components/Announcements/AnnouncementsMessageBar/announcementsMessageBar.css b/Client/src/Components/Announcements/AnnouncementsMessageBar/announcementsMessageBar.css new file mode 100644 index 000000000..bc3427302 --- /dev/null +++ b/Client/src/Components/Announcements/AnnouncementsMessageBar/announcementsMessageBar.css @@ -0,0 +1,14 @@ +:root { + --announcement-font-size-0: 13px; + --announcement-padding: 16px; + --margin: 20px; +} + +.announcement-messagebar-body { + margin: var(--margin); + font-size: var(--announcement-font-size-0); + padding: 0 var(--announcement-padding) 0 0; + justify-content: center; + align-items: center; + margin: auto; +} diff --git a/Client/src/Components/Charts/Servers/ServerStatus.jsx b/Client/src/Components/Charts/Servers/ServerStatus.jsx new file mode 100644 index 000000000..f910abf27 --- /dev/null +++ b/Client/src/Components/Charts/Servers/ServerStatus.jsx @@ -0,0 +1,37 @@ +import React from "react"; +import "./serverStatus.css"; +import PropTypes from "prop-types"; +import { useTheme } from "@mui/material"; + +/** + * @component + * @param {Object} props + * @param {string} props.title - The title text for the server status (required) + * @param {string} props.value - The value text to be displayed (required) + * @param {string} props.state - The state of the server (e.g., "online", "offline", "warning") (required) + * @returns {JSX.Element} - Renders the server status component + */ +const ServerStatus = ({ title, value, state }) => { + const theme = useTheme(); + + const fontLookup = { + default: theme.font.default.font, + }; + + const fontFamily = fontLookup["default"]; + + return ( +
+
{title}
+
{value}
+
+ ); +}; + +ServerStatus.propTypes = { + title: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + state: PropTypes.string.isRequired, +}; + +export default ServerStatus; diff --git a/Client/src/Components/Charts/Servers/serverStatus.css b/Client/src/Components/Charts/Servers/serverStatus.css new file mode 100644 index 000000000..9126d9b3b --- /dev/null +++ b/Client/src/Components/Charts/Servers/serverStatus.css @@ -0,0 +1,44 @@ +:root { + --spacing-general-0: 10px 25px; + --spacing-general-1: 20px; + --color-border-0: #eaecf0; + --border-radius-0: 4px; + --font-size-0: 1rem; + --font-size-2: 36px; + + --color-up: #17b26a; + --color-down: #f04438; + --color-pause: #fdb022; +} + +.server-status-tile { + width: 230px; + margin: var(--spacing-general-0); + margin-left: 10px; + padding: var(--spacing-general-1); + border: 1px solid var(--color-border-0); + border-radius: var(--border-radius-0); +} + +.server-status-tile-title { + font-size: var(--font-size-0); + font-weight: bold; + margin-bottom: 25px; +} + +.server-status-tile-value { + font-size: var(--font-size-2); + font-weight: bolder; +} + +.up { + color: var(--color-up); +} + +.down { + color: var(--color-down); +} + +.pause { + color: var(--color-pause); +} diff --git a/Client/src/Components/Charts/Statistics/Statistic.jsx b/Client/src/Components/Charts/Statistics/Statistic.jsx new file mode 100644 index 000000000..99ad2066f --- /dev/null +++ b/Client/src/Components/Charts/Statistics/Statistic.jsx @@ -0,0 +1,35 @@ +import React from "react"; +import "./statistic.css"; +import PropTypes from "prop-types"; +import { useTheme } from "@mui/material"; + +/** + * @component + * @param {Object} props + * @param {string} props.title - The title text for the statistic (required) + * @param {string} props.value - The numerical or textual value for the statistic (required) + * @returns {JSX.Element} - Renders the statistic component + */ +const Statistic = ({ title, value }) => { + const theme = useTheme(); + + const fontLookup = { + default: theme.font.default.font, + }; + + const fontFamily = fontLookup["default"]; + + return ( +
+
{title}
+
{value}
+
+ ); +}; + +Statistic.propTypes = { + title: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, +}; + +export default Statistic; diff --git a/Client/src/Components/Charts/Statistics/statistic.css b/Client/src/Components/Charts/Statistics/statistic.css new file mode 100644 index 000000000..a8834199d --- /dev/null +++ b/Client/src/Components/Charts/Statistics/statistic.css @@ -0,0 +1,31 @@ +:root { + --stats-spacing-general-0: 5px 10px; + --color-border-0: #eaecf0; + --border-radius-0: 4px; + --font-size-0: 1rem; + --font-size-1: 13px; + --font-color-0: #344054; + --margin-bottom-0: 10px; +} + +.statistic-tile { + margin: var(--stats-spacing-general-0); + padding: var(--stats-spacing-general-0); + border: 1px solid var(--color-border-0); + border-radius: var(--border-radius-0); + + width: 180px; +} + +.statistic-tile-title { + font-size: var(--font-size-0); + color: var(--font-color-0); + margin-bottom: var(--margin-bottom-0); +} + +.statistic-tile-value { + font-size: var(--font-size-1); + color: var(--font-color-0); + font-weight: 700; + margin-bottom: var(--margin-bottom-0); +} diff --git a/Client/src/Components/Icons/ComplexAlert/ComplexAlert.jsx b/Client/src/Components/Icons/ComplexAlert/ComplexAlert.jsx new file mode 100644 index 000000000..67af3ab88 --- /dev/null +++ b/Client/src/Components/Icons/ComplexAlert/ComplexAlert.jsx @@ -0,0 +1,36 @@ +import "./complexAlert.css"; +import React from "react"; +import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline"; +import PropTypes from "prop-types"; + +/** + * @component + * @param {Object} props + * @param {"red" | "green"} props.theme - The color theme for the alert (required) - must be either "red" or "green" + * @returns {JSX.Element} - Renders the complex alert component with dynamic color theme + */ +const ComplexAlert = ({ theme }) => { + if (theme === "red") { + return ( +
+
+ +
+
+ ); + } else if (theme === "green") { + return ( +
+
+ +
+
+ ); + } +}; + +ComplexAlert.propTypes = { + theme: PropTypes.string.isRequired, +}; + +export default ComplexAlert; diff --git a/Client/src/Components/Icons/ComplexAlert/complexAlert.css b/Client/src/Components/Icons/ComplexAlert/complexAlert.css new file mode 100644 index 000000000..ab3cea714 --- /dev/null +++ b/Client/src/Components/Icons/ComplexAlert/complexAlert.css @@ -0,0 +1,44 @@ +.icon-holder-outer-red { + width: 38px; + height: 38px; + border-radius: 50%; + border: 2px solid #d92d2019; + display: flex; + justify-content: center; + align-items: center; +} + +.icon-holder-inner-red { + width: 28px; + height: 28px; + border-radius: 50%; + border: 2px solid #d92d204d; + display: flex; + justify-content: center; + align-items: center; +} + +.icon-holder-outer-green { + width: 38px; + height: 38px; + border-radius: 50%; + border: 2px solid #07945519; + display: flex; + justify-content: center; + align-items: center; +} + +.icon-holder-inner-green { + width: 28px; + height: 28px; + border-radius: 50%; + border: 2px solid #0794554d; + display: flex; + justify-content: center; + align-items: center; +} + +.icon-core { + width: 20px; + height: 20px; +} diff --git a/Client/src/Components/PopupModals/DualButtonPopupModal/DualButtonPopupModal.jsx b/Client/src/Components/PopupModals/DualButtonPopupModal/DualButtonPopupModal.jsx new file mode 100644 index 000000000..42a3f2a8a --- /dev/null +++ b/Client/src/Components/PopupModals/DualButtonPopupModal/DualButtonPopupModal.jsx @@ -0,0 +1,55 @@ +import "./dualButtonPopupModal.css"; +import React from "react"; +import PropTypes from "prop-types"; +import { useTheme } from "@mui/material"; + +/** + * @component + * @param {Object} props + * @param {boolean} [props.open=true] - Controls the visibility of the modal (defaults to true) + * @param {string} props.subject - The title text for the modal (required) + * @param {string} props.description - The description text for the modal (required) + * @param {string} props.esc - The text for the discard button (usually "Cancel", "Dismiss" or "Discard") (required) + * @param {string} props.save - The text for the save button (required) + * @returns {JSX.Element} - Renders the dual button popup modal component + */ +const DualButtonPopupModal = ({ + open = true, + subject, + description, + esc, + save, +}) => { + const theme = useTheme(); + + const fontLookup = { + default: theme.font.default.font, + }; + + const fontFamily = fontLookup["default"]; + + return ( +
+
{subject}
+
{description}
+
+
+ + +
+
+ ); +}; + +DualButtonPopupModal.propTypes = { + open: PropTypes.bool, + subject: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + esc: PropTypes.string.isRequired, + save: PropTypes.string.isRequired, +}; + +export default DualButtonPopupModal; diff --git a/Client/src/Components/PopupModals/DualButtonPopupModal/dualButtonPopupModal.css b/Client/src/Components/PopupModals/DualButtonPopupModal/dualButtonPopupModal.css new file mode 100644 index 000000000..92ac7f5d6 --- /dev/null +++ b/Client/src/Components/PopupModals/DualButtonPopupModal/dualButtonPopupModal.css @@ -0,0 +1,60 @@ +:root { + --popup-color-border-0: #f2f2f2; + --popup-color-0: #101828; + --popup-color-1: #475467; + --color-0: #1570ef; + --color-1: #175cd3; +} + +.dual-button-popup-modal-holder { + width: 380px; + height: 130px; + margin: 10px 20px; + padding: 30px; + box-shadow: 0px 8px 8px -4px rgba(16, 24, 40, 0.03), + 0px 20px 24px -4px rgba(16, 24, 40, 0.08); + border: 1px solid var(--popup-color-border-0); + border-radius: 4px; +} + +.dual-button-popup-modal-subject { + color: var(--popup-color-0); + font-weight: bolder; + font-size: 16px; +} + +.dual-button-popup-modal-description { + color: var(--popup-color-1); + font-size: 13px; + margin-top: 10px; +} + +.dual-modal-spacing { + height: 40px; +} + +.dual-button-popup-modal-controllers { + display: flex; + justify-content: end; +} + +.blue-save-button { + width: 145px; + height: 35px; + padding: 5px 20px; + background-color: var(--color-0); + border: 1px solid var(--color-1); + border-radius: 4px; + margin: 5px 0; + margin-left: 20px; + color: white; + cursor: pointer; + font-size: 13px; +} + +.transparent-discard-button { + border: none; + background-color: transparent; + margin: 5px 10px; + font-size: 13px; +} diff --git a/Client/src/Components/PopupModals/DualButtonPopupModalWithTextfields/DualButtonPopupModalWithTextfields.jsx b/Client/src/Components/PopupModals/DualButtonPopupModalWithTextfields/DualButtonPopupModalWithTextfields.jsx new file mode 100644 index 000000000..b8b424323 --- /dev/null +++ b/Client/src/Components/PopupModals/DualButtonPopupModalWithTextfields/DualButtonPopupModalWithTextfields.jsx @@ -0,0 +1,73 @@ +import "./dualButtonPopupModalWithTextfields.css"; +import React from "react"; +import CloseIcon from "@mui/icons-material/Close"; +import PropTypes from "prop-types"; +import { useTheme } from "@mui/material"; + +/** + * @component + * @param {Object} props + * @param {string} props.title - The title text for the modal (required) + * @returns {JSX.Element} - Renders a single text field component within a popup modal + */ +const PopupModalTextfield = ({ title }) => { + const theme = useTheme(); + + const fontLookup = { + default: theme.font.default.font, + }; + + const fontFamily = fontLookup["default"]; + + return ( +
+
{title}
+ +
+ ); +}; + +/** + * @component + * @param {Object} props + * @param {string} props.title - The title text for the modal (required) + * @param {string} props.esc - The text for the cancel button (required) + * @param {string} props.save - The text for the save button (required) + * @returns {JSX.Element} - Renders the dual button popup modal component with text fields + */ +const DualButtonPopupModalWithTextfields = ({ title, esc, save }) => { + const theme = useTheme(); + + const fontLookup = { + default: theme.font.default.font, + }; + + const fontFamily = fontLookup["default"]; + + return ( +
+
+
{title}
+
+ +
+
+
+ +
+
+
+ + +
+
+ ); +}; + +DualButtonPopupModalWithTextfields.propTypes = { + title: PropTypes.string.isRequired, + esc: PropTypes.string.isRequired, + save: PropTypes.string.isRequired, +}; + +export default DualButtonPopupModalWithTextfields; diff --git a/Client/src/Components/PopupModals/DualButtonPopupModalWithTextfields/dualButtonPopupModalWithTextfields.css b/Client/src/Components/PopupModals/DualButtonPopupModalWithTextfields/dualButtonPopupModalWithTextfields.css new file mode 100644 index 000000000..a1be3cd3a --- /dev/null +++ b/Client/src/Components/PopupModals/DualButtonPopupModalWithTextfields/dualButtonPopupModalWithTextfields.css @@ -0,0 +1,61 @@ +:root { + --popup-color-with-input-border-0: #f2f2f2; + --popup-color-with-input-border-1: #d0d5dd; + --popup-modal-with-input-title-color: #344054; +} + +.popup-modal-holder { + width: 480px; + font-family: var(--popup-font-family-0); + padding: 50px 40px; + border-radius: 4px; + margin: 10px 20px; + border: 1px solid var(--popup-color-with-input-border-0); + box-shadow: 0px 8px 8px -4px rgba(16, 24, 40, 0.03), + 0px 20px 24px -4px rgba(16, 24, 40, 0.08); +} + +.popup-modal-header { + display: flex; + justify-content: space-between; + font-weight: bold; +} + +.popup-modal-close { + cursor: pointer; +} + +.spacing-height { + height: 30px; +} + +.popup-modal-input-title { + margin-bottom: 15px; + color: var(--popup-modal-with-input-title-color); +} + +.popup-modal-text-field { + width: 100%; + height: 35px; + border: 1px solid var(--popup-color-with-input-border-1); + border-radius: 4px; +} + +.popup-modal-controllers { + display: flex; + justify-content: end; +} + +.white-cancel-button { + border: none; + background-color: transparent; + font-family: var(--popup-font-family-0); + margin: 5px 10px; + width: 145px; + height: 35px; + padding: 5px 20px; + border: 1px solid var(--popup-color-with-input-border-1); + border-radius: 4px; + cursor: pointer; + font-size: 13px; +} diff --git a/Client/src/Components/TextFields/Description/DescriptionTextField.jsx b/Client/src/Components/TextFields/Description/DescriptionTextField.jsx new file mode 100644 index 000000000..7f3ba908a --- /dev/null +++ b/Client/src/Components/TextFields/Description/DescriptionTextField.jsx @@ -0,0 +1,47 @@ +import React from "react"; +import "./descriptionTextField.css"; +import { TextField } from "@mui/material"; +import PropTypes from "prop-types"; +import { useTheme } from "@mui/material"; + +/** + * @component + * @param {Object} props + * @param {string} props.hintText - The hint text displayed within the text field (required) + * @param {boolean} props.hasError - A flag indicating if the text field has an error (required) + * @returns {JSX.Element} - Renders the description text field component + */ +const DescriptionTextField = ({ hintText, hasError }) => { + const theme = useTheme(); + + const fontLookup = { + default: theme.font.default.font, + }; + + const fontFamily = fontLookup["default"]; + + return ( +
+
Description
+ +
+ ); +}; + +DescriptionTextField.propTypes = { + hintText: PropTypes.string.isRequired, + hasError: PropTypes.bool.isRequired, +}; + +export default DescriptionTextField; diff --git a/Client/src/Components/TextFields/Description/descriptionTextField.css b/Client/src/Components/TextFields/Description/descriptionTextField.css new file mode 100644 index 000000000..28616863e --- /dev/null +++ b/Client/src/Components/TextFields/Description/descriptionTextField.css @@ -0,0 +1,45 @@ +:root { + --color-title-1: #344054; + --text-field-margin-0: 10px 20px; + --description-color-0: #344054; +} + +.description-field-holder { + margin: var(--text-field-margin-0); + margin-top: 20px; +} + +.text-field-title { + color: var(--color-title-1); + font-weight: bold; + margin-bottom: 10px; + font-size: 13px; + color: var(--description-color-0); + font-weight: bold; +} + +.description-field-area { + min-width: 320px; + font-size: 13px; + border-radius: 8px; +} + +.description-field-label { + margin-top: 10px; + font-size: 11px; +} + +.with-error { + color: red; +} + +#description-field-area-helper-text { + margin-left: 0; + margin-top: 10px; + font-size: 11px; +} + +#description-field-area { + min-width: 320px; + font-size: 13px; +} diff --git a/Client/src/Components/TextFields/Email/EmailTextField.jsx b/Client/src/Components/TextFields/Email/EmailTextField.jsx new file mode 100644 index 000000000..b1428e1c5 --- /dev/null +++ b/Client/src/Components/TextFields/Email/EmailTextField.jsx @@ -0,0 +1,88 @@ +import React, { useState } from "react"; +import "./emailTextField.css"; +import { InputAdornment, TextField } from "@mui/material"; +import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline"; +import HelpOutlineIcon from "@mui/icons-material/HelpOutline"; +import { useTheme } from "@mui/material"; + +/** + * @component + * @param {Object} props + * @param {string} props.id - Unique ID for the text field (optional) + * @param {string} props.label - The label text displayed above the text field (optional) + * @param {"standard" | "outlined" | "filled"} props.variant - The variant of the text field (e.g., "outlined") (optional) + * @param {string} props.placeholder - The placeholder text displayed within the text field (optional) + * @param {"error" | "help"} [props.icon] - The type of icon to display (error or help) (optional) + * @param {string} props.helperText - The helper text displayed below the text field (optional) + * @param {boolean} props.error - A flag indicating if the text field has an error (required) + * @returns {JSX.Element} - Renders the email text field component with dynamic icon display + */ +const EmailTextField = ({ + id, + label, + variant, + placeholder, + icon, + helperText, + error, +}) => { + const theme = useTheme(); + + const fontLookup = { + default: theme.font.default.font, + }; + + const fontFamily = fontLookup["default"]; + + const [showIcon, setShowIcon] = useState(false); + + const handleMouseEnter = () => setShowIcon(true); + const handleMouseLeave = () => setShowIcon(false); + + return ( +
+
Email
+
+ +
+ {error && showIcon && ( + + )} + {error && !showIcon && ( + + )} + {!error && showIcon && ( + + )} +
+ + ), + }} + helperText={helperText} + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} + /> +
+
+ ); +}; + +export default EmailTextField; diff --git a/Client/src/Components/TextFields/Email/emailTextField.css b/Client/src/Components/TextFields/Email/emailTextField.css new file mode 100644 index 000000000..ff38a29df --- /dev/null +++ b/Client/src/Components/TextFields/Email/emailTextField.css @@ -0,0 +1,41 @@ +:root { + --textfield-color-0: #344054; +} + +.email-text-field { + position: relative; + margin: 10px 20px; +} + +#outlined-basic { + width: 320px; + height: 14px; + font-size: 13px; + padding: 8px 12px; +} + +.email-text-field-title { + margin: 0 20px; + margin-top: 30px; + font-size: 13px; + color: var(--textfield-color-0); + font-weight: bold; +} + +#outlined-basic-helper-text { + margin-left: 0; + margin-top: 10px; + font-size: 11px; +} + +.text-field-icon { + width: 16px; + height: 16px; +} + +.icon-holder { + width: 16px; + height: 16px; + padding: 0; + margin: 0; +} diff --git a/Client/src/Components/TextFields/Website/WebsiteTextField.jsx b/Client/src/Components/TextFields/Website/WebsiteTextField.jsx new file mode 100644 index 000000000..d18e33dd9 --- /dev/null +++ b/Client/src/Components/TextFields/Website/WebsiteTextField.jsx @@ -0,0 +1,72 @@ +import React, { useState } from "react"; +import "./websiteTextField.css"; +import HelpOutlineIcon from "@mui/icons-material/HelpOutline"; +import ContentCopyIcon from "@mui/icons-material/ContentCopy"; +import { useTheme } from "@mui/material"; + +/** + * @component + * @param {Object} props + * @param {boolean} [props.showHelp=true] - Controls the visibility of the help icon (defaults to true) + * @param {boolean} [props.hasCopyButton=false] - Controls the visibility of the copy button (defaults to false) + * @param {string} props.hintText - The hint text displayed below the text field (required) + * @returns {JSX.Element} - Renders the website text field component with optional help and copy features + */ +const WebsiteTextField = ({ + showHelp = true, + hasCopyButton = false, + hintText, +}) => { + const theme = useTheme(); + + const fontLookup = { + default: theme.font.default.font, + }; + + const fontFamily = fontLookup["default"]; + + const [copied, setCopied] = useState(false); + + const handleCopy = () => { + setCopied(true); + setTimeout(() => setCopied(false), 1000); + }; + + return ( +
+
Website
+
+ + + {showHelp && ( +
+ +
+ )} + {hasCopyButton && ( +
+ + {copied ? " Copied " : " Copy "} +
+ )} +
+ +
+ ); +}; + +export default WebsiteTextField; diff --git a/Client/src/Components/TextFields/Website/websiteTextField.css b/Client/src/Components/TextFields/Website/websiteTextField.css new file mode 100644 index 000000000..66cdf4fa4 --- /dev/null +++ b/Client/src/Components/TextFields/Website/websiteTextField.css @@ -0,0 +1,82 @@ +:root { + --website-color-0: #344054; + --border-color: #d0d5dd; + --label-font-color: #475467; + --icon-color: #98a2b3; + --text-field-margin-0: 10px 20px; + --text-field-margin-1: 10px 20px 5px; +} + +.website-textfield-title { + margin: 0 20px; + margin-top: 30px; + font-size: 13px; + color: var(--website-color-0); + font-weight: bold; +} + +.website-textfield { + display: flex; + align-items: center; + position: relative; + margin: var(--text-field-margin-1); +} + +.website-textfield-hint { + margin: var(--text-field-margin-0); + font-size: 11px; +} + +.website-label { + padding: 6px 20px 6px 10px; + border: solid 1px var(--border-color); + border-right: none; + border-radius: 4px 0 0 4px; + font-size: 13px; + text-align: center; + display: flex; + justify-content: center; + align-items: center; + color: var(--label-font-color); +} + +.website-url { + width: 248px; + height: 31.5px; + padding: 0px 15px; + border: solid 1px var(--border-color); +} + +.website-icon-holder { + width: 40px; + height: 33px; + padding: 5px; + border: solid 1px var(--border-color); + border-left: none; + text-align: center; + display: flex; + justify-content: center; + align-items: center; +} + +.copy-to-clipboard-button { + padding: 5.5px 20px 6px 10px; + border: solid 1px var(--border-color); + border-left: none; + border-radius: 0 8px 8px 0; + font-size: 13px; + text-align: center; + display: flex; + justify-content: space-evenly; + align-items: center; + color: var(--label-font-color); + cursor: pointer; +} + +.copy-icon { + margin-right: 5px; +} + +.with-no-copy-button { + display: none; +} diff --git a/Client/src/Components/Tooltips/TooltipWithTail/TooltipWithTail.jsx b/Client/src/Components/Tooltips/TooltipWithTail/TooltipWithTail.jsx new file mode 100644 index 000000000..1b1fb8b4d --- /dev/null +++ b/Client/src/Components/Tooltips/TooltipWithTail/TooltipWithTail.jsx @@ -0,0 +1,45 @@ +import "./tooltipWithTail.css"; +import React from "react"; +import { styled } from "@mui/material/styles"; +import Tooltip, { tooltipClasses } from "@mui/material/Tooltip"; +import HelpOutlineIcon from "@mui/icons-material/HelpOutline"; + +const CustomizedTooltip = styled( + ({ className, placement, arrow, ...props }) => ( + + ) +)(({ theme }) => ({ + [`& .${tooltipClasses.tooltip}`]: { + backgroundColor: "black", + }, + [`& .${tooltipClasses.arrow}`]: { + color: "black", + }, +})); + +function TooltipWithTail({ placement, arrow = false, title, text }) { + return ( +
+ + {title &&
{title}
} +
{text}
+ + } + className="tooltip-holder" + > + +
+
+ ); +} + +export default TooltipWithTail; diff --git a/Client/src/Components/Tooltips/TooltipWithTail/tooltipWithTail.css b/Client/src/Components/Tooltips/TooltipWithTail/tooltipWithTail.css new file mode 100644 index 000000000..1b1adf507 --- /dev/null +++ b/Client/src/Components/Tooltips/TooltipWithTail/tooltipWithTail.css @@ -0,0 +1,17 @@ +:root { + --tooltip-color-0: #d0d5dd; + --tooltip-margin: 5px; + --tooltip-font-size: 12px; +} + +.tooltip-title { + font-size: var(--tooltip-font-size); + font-weight: bold; + margin: var(--tooltip-margin); +} + +.tooltip-description { + font-size: var(--tooltip-font-size); + color: var(--tooltip-color-0); + margin: var(--tooltip-margin); +} diff --git a/Client/src/Pages/PlayGround/PlayGround-Charts.jsx b/Client/src/Pages/PlayGround/PlayGround-Charts.jsx new file mode 100644 index 000000000..906f2a84a --- /dev/null +++ b/Client/src/Pages/PlayGround/PlayGround-Charts.jsx @@ -0,0 +1,25 @@ +import React from "react"; +import "./playGround.css"; +import Statistic from "../../Components/Charts/Statistics/Statistic"; +import ServerStatus from "../../Components/Charts/Servers/ServerStatus"; + +function PlayGroundCharts() { + return ( + <> +
+ + + + +
+
+
+ + + +
+ + ); +} + +export default PlayGroundCharts; diff --git a/Client/src/Pages/PlayGround/PlayGround-Popup-Modals.jsx b/Client/src/Pages/PlayGround/PlayGround-Popup-Modals.jsx new file mode 100644 index 000000000..a3b89349c --- /dev/null +++ b/Client/src/Pages/PlayGround/PlayGround-Popup-Modals.jsx @@ -0,0 +1,25 @@ +import React from "react"; +import DualButtonPopupModal from "../../Components/PopupModals/DualButtonPopupModal/DualButtonPopupModal"; +import DualButtonPopupModalWithTextfields from "../../Components/PopupModals/DualButtonPopupModalWithTextfields/DualButtonPopupModalWithTextfields"; + +function PlayGroundPopupModals() { + return ( +
+ +
+
+ +
+ ); +} + +export default PlayGroundPopupModals; diff --git a/Client/src/Pages/PlayGround/PlayGround-Tooltips.jsx b/Client/src/Pages/PlayGround/PlayGround-Tooltips.jsx new file mode 100644 index 000000000..3713f889c --- /dev/null +++ b/Client/src/Pages/PlayGround/PlayGround-Tooltips.jsx @@ -0,0 +1,34 @@ +import React from "react"; +import TooltipWithTail from "../../Components/Tooltips/TooltipWithTail/TooltipWithTail"; +import "./playGround.css"; + +function PlayGroundTooltips() { + return ( +
+
+ +
+ +
+ +
+ +
+ ); +} + +export default PlayGroundTooltips; diff --git a/Client/src/Pages/PlayGround/PlayGround-announcements.jsx b/Client/src/Pages/PlayGround/PlayGround-announcements.jsx new file mode 100644 index 000000000..aa4bd1913 --- /dev/null +++ b/Client/src/Pages/PlayGround/PlayGround-announcements.jsx @@ -0,0 +1,73 @@ +import "./playGround.css"; +import React from "react"; +import AnnouncementsDualButtonWithIcon from "../../Components/Announcements/AnnouncementsDualButtonWithIcon/AnnouncementsDualButtonWithIcon"; +import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; +import ComplexAlert from "../../Components/Icons/ComplexAlert/ComplexAlert"; +import AnnouncementsMessageBar from "../../Components/Announcements/AnnouncementsMessageBar/AnnouncementsMessageBar"; +import AnnouncementsDualButton from "../../Components/Announcements/AnnouncementsDualButton/AnnouncementsDualButton"; +import AnnouncementUpdateSubscription from "../../Components/Announcements/AnnouncementUpdateSubscription/AnnouncementUpdateSubscription"; + +function PlayGroundAnnouncements() { + return ( +
+ + +
+ } + subject="We’ve just released a new feature" + body="Lorem ipsum dolor sit amet consectetur adipisicing elit. Aliquid + pariatur, ipsum dolor." + esc="Dismiss" + primary="View changes" + /> +
+
+ } + subject="There was a problem with that action" + body="Lorem ipsum dolor sit amet consectetur adipisicing elit. Aliquid + pariatur, ipsum dolor." + esc="Dismiss" + primary="Learn more" + /> +
+
+ } + subject="Successfully updated profile" + body="Lorem ipsum dolor sit amet consectetur adipisicing elit. Aliquid + pariatur, ipsum dolor." + esc="Dismiss" + primary="Learn more" + /> +
+
+ +
+
+ +
+
+ +
+
+
+ ); +} + +export default PlayGroundAnnouncements; diff --git a/Client/src/Pages/PlayGround/PlayGround.jsx b/Client/src/Pages/PlayGround/PlayGround.jsx new file mode 100644 index 000000000..b5e439aed --- /dev/null +++ b/Client/src/Pages/PlayGround/PlayGround.jsx @@ -0,0 +1,126 @@ +import React from "react"; +import EmailTextField from "../../Components/TextFields/Email/EmailTextField"; +import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline"; +import HelpOutlineIcon from "@mui/icons-material/HelpOutline"; +import WebsiteTextField from "../../Components/TextFields/Website/WebsiteTextField"; +import DescriptionTextField from "../../Components/TextFields/Description/DescriptionTextField"; +import AnnouncementsDualButton from "../../Components/Announcements/AnnouncementsDualButton/AnnouncementsDualButton"; +import AnnouncementsDualButtonWithIcon from "../../Components/Announcements/AnnouncementsDualButtonWithIcon/AnnouncementsDualButtonWithIcon"; +import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; +import ComplexAlert from "../../Components/Icons/ComplexAlert/ComplexAlert"; +import AnnouncementsMessageBar from "../../Components/Announcements/AnnouncementsMessageBar/AnnouncementsMessageBar"; +import AnnouncementUpdateSubscription from "../../Components/Announcements/AnnouncementUpdateSubscription/AnnouncementUpdateSubscription"; +import PlayGroundCharts from "./PlayGround-Charts"; +import PlayGroundPopupModals from "./PlayGround-Popup-Modals"; +import PlayGroundTooltips from "./PlayGround-Tooltips"; + +// This Component is just for the development and test +// purposes and just to see what my components look like while development +// and will be removed in the end +function PlayGround() { + return ( +
+ {/* Default state of Email Text Fields */} + + {/* Email Text Field when validation error occures */} + } + helperText="This is an error message" + /> + {/* Email Text Field while mouse hover */} + } + helperText="This is a hint text to help user." + /> + +
+ {/* Now, illustration of the Website text fields */} + + + + +
+ {/* Now, illustration of the Description text fields */} + + + + + +
+ } + subject="We’ve just released a new feature" + body="Lorem ipsum dolor sit amet consectetur adipisicing elit. Aliquid + pariatur, ipsum dolor." + esc="Dismiss" + primary="View changes" + /> + + } + subject="There was a problem with that action" + body="Lorem ipsum dolor sit amet consectetur adipisicing elit. Aliquid + pariatur, ipsum dolor." + esc="Dismiss" + primary="Learn more" + /> + + } + subject="Successfully updated profile" + body="Lorem ipsum dolor sit amet consectetur adipisicing elit. Aliquid + pariatur, ipsum dolor." + esc="Dismiss" + primary="Learn more" + /> + + + + + + +
+ +
+ +
+ + + ); +} + +export default PlayGround; diff --git a/Client/src/Pages/PlayGround/playGround.css b/Client/src/Pages/PlayGround/playGround.css new file mode 100644 index 000000000..cd6d5e87b --- /dev/null +++ b/Client/src/Pages/PlayGround/playGround.css @@ -0,0 +1,19 @@ +.play-ground-charts { + display: flex; +} + +.play-ground-charts-spacing { + height: 110px; +} + +.tooltip-playground { + margin: auto; + padding: 5%; + width: 5%; + justify-content: center; + text-align: center; +} + +.playground { + padding: 40px; +} diff --git a/Client/src/Utils/Theme.js b/Client/src/Utils/Theme.js index 99a442dfb..068e24572 100644 --- a/Client/src/Utils/Theme.js +++ b/Client/src/Utils/Theme.js @@ -16,6 +16,10 @@ const labelRed = "#F04438"; //Section colros const sectionBorder = "#D0D5DD"; const sectionBg = "#F8F9F8"; + +// Global Font Family +const fontFamilyDefault = '"Roboto", "Helvetica", "Arial", sans-serif'; + const theme = createTheme({ palette: { primary: { @@ -48,6 +52,11 @@ const theme = createTheme({ bgColor: sectionBg, }, }, + font :{ + default: { + font: fontFamilyDefault, + }, + }, shape: { borderRadius: 4, }, diff --git a/Client/src/assets/icons/icon-red-error-0.svg b/Client/src/assets/icons/icon-red-error-0.svg new file mode 100644 index 000000000..f052582ba --- /dev/null +++ b/Client/src/assets/icons/icon-red-error-0.svg @@ -0,0 +1,3 @@ + + + diff --git a/README.md b/README.md index 3397a324d..92e1b7bb1 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,11 @@ BlueWave uptime monitoring application - Clone this repository to your local machine +1. [Installation (Client)](#client) +1. [Installation (Server)](#server) +1. [Configuration(Server)](#config-server) +1. [Endpoints](#endpoints) + --- ### Client @@ -30,15 +35,17 @@ BlueWave uptime monitoring application --- -#### Configuration +#### Configuration {#config-server} Configure the server with the following environmental variables -| ENV Varialbe Name | Required/Optional | Type | Description | -| -------------------- | ----------------- | ------ | --------------------------------- | -| DB_CONNECTION_STRING | Required | string | Specfies URL for MongoDB Database | -| MAILERSEND_API_KEY | Required | string | Specfies API KEY for MailerSend service | -| SYSTEM_EMAIL_ADDRESS | Required | string | Specfies System email to be used in emailing service | +| ENV Varialbe Name | Required/Optional | Type | Description | Accepted Values | +| -------------------- | ----------------- | ------- | ---------------------------------------------------- | ------------------- | +| DB_TYPE | Optional | string | Specify DB to use | `MongoDB \| FakeDB` | +| DB_CONNECTION_STRING | Required | string | Specfies URL for MongoDB Database | | +| PORT | Optional | Integer | Specifies Port for Server | | +| MAILERSEND_API_KEY | Required | string | Specfies API KEY for MailerSend service | | +| SYSTEM_EMAIL_ADDRESS | Required | string | Specfies System email to be used in emailing service | | --- @@ -64,9 +71,28 @@ Example: {success: true, msg: "Successful Request", data: {test: testData}} ``` +--- + ##### Data Types -###### Monitor +
+User + +| Name | Type | Notes | +| ------------- | --------- | --------------------- | +| firstname | `string` | First name | +| lastname | `string` | Last name | +| email | `string` | User's email | +| profilePicUrl | `string` | URL to User's picture | +| isActive | `boolean` | Default to `true` | +| isVerified | `boolean` | Default to `false` | +| updated_at | `Date` | Last update time | +| created_at | `Date` | Time created at | + +
+ +
+Monitor | Name | Type | Notes | | ----------- | --------- | ---------------------------------------- | @@ -79,51 +105,186 @@ Example: | updatedAt | `Date` | Last time the monitor was updated | | CreatedAt | `Date` | When the monitor was updated | ---- - -##### GET /api/v1/monitors - -###### Response - -| Status Code | Type | Description | -| ----------- | --------------------- | -------------------------------- | -| 200 | Response with Payload | Response with a list of monitors | - -###### Payload - -| Type | Notes | -| ---------------- | ----------------- | -| `Array[Monitor]` | Array of monitors | +
--- -##### GET /api/v1/monitor/:id +
+POST /api/v1/auth/register -###### Response +##### Method/Headers -| Status Code | Type | Description | -| ----------- | --------------------- | ------------------------------ | -| 200 | Response with Payload | Response with a single monitor | +> | Method/Headers | Value | +> | -------------- | ---------------- | +> | Method | POST | +> | content-type | application/json | -###### Payload +##### Body -| Type | Notes | -| --------- | --------------------------------------------------- | -| `Monitor` | Single monitor with the id in the request parameter | +> | Name | Type | Notes | +> | --------- | -------- | ------------------- | +> | firstname | `string` | | +> | lastname | `string` | +> | email | `string` | Valid email address | +> | password | `string` | Min 8 chars | + +##### Response Payload + +> | Type | Notes | +> | ------ | ---------- | +> | `User` | User model | + +##### Sample CURL request + +``` +curl --request POST \ + --url http://localhost:5000/api/v1/auth/register \ + --header 'Content-Type: application/json' \ + --data '{ + "firstname" : "User First Name", + "lastname": "User Last Name", + "email" : "user@gmail.com", + "password": "user_password" +}' +``` + +##### Sample Response + +```json +{ + "success": true, + "msg": "User created}", + "data": { + "_id": "6645079aae0b439371913972", + "firstname": "User First Name", + "lastname": "User Last Name", + "email": "user@gmail.com", + "isActive": true, + "isVerified": false, + "updated_at": "2024-05-15T19:06:02.720Z", + "created_at": "2024-05-15T19:06:02.720Z", + "__v": 0 + } +} +``` + +
--- -##### GET /api/v1/monitors/user/:userId +
+POST /api/v1/auth/login -| Status Code | Type | Description | -| ----------- | --------------------- | -------------------------------- | -| 200 | Response with Payload | Response with a list of monitors | +##### Method/Headers -###### Payload +> | Method/Headers | Value | +> | -------------- | ---------------- | +> | Method | POST | +> | content-type | application/json | -| Type | Notes | -| ---------------- | ------------------------------------------------------------------ | -| `Array[Monitor]` | Array of monitors created by user with userId specified in request | +##### Body + +> | Name | Type | Notes | +> | -------- | -------- | ------------------- | +> | email | `string` | Valid email address | +> | password | `string` | Min 8 chars | + +##### Response Payload + +> | Type | Notes | +> | ------ | ---------- | +> | `User` | User model | + +##### Sample CURL request + +``` +curl --request POST \ + --url http://localhost:5000/api/v1/auth/login \ + --header 'Content-Type: application/json' \ + --data '{ + "email" : "user@gmail.com", + "password": "user_password" +}' +``` + +##### Sample response + +```json +{ + "success": true, + "msg": "Found user", + "data": { + "_id": "6644fb9bae0b439371913969", + "firstname": "User First Name", + "lastname": "User Last Name", + "email": "user@gmail.com", + "isActive": true, + "isVerified": false, + "updated_at": "2024-05-15T18:14:51.049Z", + "created_at": "2024-05-15T18:14:51.049Z", + "__v": 0 + } +} +``` + +
+ +--- + +
+GET /api/v1/monitors + +##### Method/Headers + +> | Method/Headers | Value | +> | -------------- | ----- | +> | Method | GET | + +##### Response Payload + +> | Type | Notes | +> | ---------------- | ----------------- | +> | `Array[Monitor]` | Array of monitors | + +
+ +--- + +
+GET /api/v1/monitor/{id} + +###### Method/Headers + +> | Method/Headers | Value | +> | -------------- | ----- | +> | Method | GET | + +###### Response Payload + +> | Type | Notes | +> | --------- | --------------------------------------------------- | +> | `Monitor` | Single monitor with the id in the request parameter | + +
+ +--- + +
+GET /api/v1/monitors/user/{userId} + +###### Method/Headers + +> | Method/Headers | Value | +> | -------------- | ----- | +> | Method | GET | + +###### Request Payload + +> | Type | Notes | +> | ---------------- | ------------------------------------------------------------------ | +> | `Array[Monitor]` | Array of monitors created by user with userId specified in request | + +
## Contributors diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js index eba12db7d..e8fdc739e 100644 --- a/Server/controllers/authController.js +++ b/Server/controllers/authController.js @@ -1,7 +1,7 @@ const express = require("express"); const UserModel = require("../models/user"); -const { registerValidation } = require("../validation/joi"); -const logger = require('../utils/logger') +const { registerValidation, loginValidation } = require("../validation/joi"); +const logger = require("../utils/logger"); /** * @function @@ -23,28 +23,73 @@ const registerController = async (req, res) => { try { const isUser = await req.db.getUserByEmail(req, res); if (isUser) { - logger.warning("User already exists!", { "service": "auth", "userId": isUser._id }); + logger.error("User already exists!", { + service: "auth", + userId: isUser._id, + }); return res .status(400) .json({ success: false, msg: "User already exists!" }); } } catch (error) { - logger.error(error.message, { "service": "auth" }); + logger.error(error.message, { service: "auth" }); return res.status(500).json({ success: false, msg: error.message }); } - try { // Create a new user const newUser = await req.db.insertUser(req, res); // TODO: Send an email to user // Will add this later - logger.info("New user created!", { "service": "auth", "userId": newUser._id }); - return res.json({ success: true, msg: "User created}", data: newUser }); + logger.info("New user created!", { service: "auth", userId: newUser._id }); + return res + .status(200) + .json({ success: true, msg: "User created}", data: newUser }); } catch (error) { - logger.error(error.message, { "service": "auth" }); + logger.error(error.message, { service: "auth" }); return res.status(500).json({ success: false, msg: error.message }); } }; -module.exports = { registerController }; +// ************************** +// Handles logging in a user +// Returns a user at the moment, but will likely return a JWT in the future +// ************************** +const loginController = async (req, res) => { + try { + // Validate input + await loginValidation.validateAsync(req.body); + + // Check if user exists + const user = await req.db.getUserByEmail(req, res); + + // If user not found, throw an error + if (!user) { + throw new Error("User not found!"); + } + // Compare password + const match = await user.comparePassword(req.body.password); + console.log(user); + if (!match) { + throw new Error("Password does not match!"); + } + + // Copy user to remove password. More memory, but better than mutating user? + // We can move this to the DB layer if we want + // Probably not neccessary at all as we'll likely return a JWT instead of a user + const userWithoutPassword = { ...user._doc }; + delete userWithoutPassword.password; + + // Happy path, return user + // In the future we'll probably return a JWT instead of a user + return res + .status(200) + .json({ success: true, msg: "Found user", data: userWithoutPassword }); + } catch (error) { + // Anything else should be an error + logger.error(error.message, { service: "auth" }); + return res.status(500).json({ success: false, msg: error.message }); + } +}; + +module.exports = { registerController, loginController }; diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index c85da0f9d..8b2f72ab1 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -1,26 +1,43 @@ const { - getMonitorsByIdValidation, + getMonitorByIdValidation, getMonitorsByUserIdValidation, + monitorValidation, } = require("../validation/joi"); -const logger = require('../utils/logger') -// Gets all monitors +const logger = require("../utils/logger"); +const Monitor = require("../models/Monitor"); + +/** + * Returns all monitors + * @async + * @param {Express.Request} req + * @param {Express.Response} res + * @returns {Promise} + * @throws {Error} + */ const getAllMonitors = async (req, res) => { try { const monitors = await req.db.getAllMonitors(); return res.json({ success: true, msg: "Monitors found", data: monitors }); } catch (error) { - logger.error(error.message, { "service": "monitor" }); + logger.error(error.message, { service: "monitor" }); return res.status(500).json({ success: false, msg: error.message }); } }; -// Get a monitor by ID +/** + * Returns monitor with matching ID + * @async + * @param {Express.Request} req + * @param {Express.Response} res + * @returns {Promise} + * @throws {Error} + */ const getMonitorById = async (req, res) => { - const { error } = getMonitorsByIdValidation.validate(req.params); + const { error } = getMonitorByIdValidation.validate(req.params); if (error) { return res - .status(400) + .status(422) .json({ success: false, msg: error.details[0].message }); } @@ -29,33 +46,143 @@ const getMonitorById = async (req, res) => { const monitor = await req.db.getMonitorById(monitorId); return res.json({ success: true, msg: "Monitor found", data: monitor }); } catch (error) { - logger.error(error.message, { "service": "monitor" }); + logger.error(error.message, { service: "monitor" }); return res.status(500).json({ success: false, msg: error.message }); } }; -// Gets a monitor by user ID +/** + * Returns all monitors belong to User with UserID + * @async + * @param {Express.Request} req + * @param {Express.Response} res + * @returns {Promise} + * @throws {Error} + */ const getMonitorsByUserId = async (req, res) => { const { error } = getMonitorsByUserIdValidation.validate(req.params); if (error) { return res - .status(400) + .status(422) .json({ success: false, msg: error.details[0].message }); } try { const userId = req.params.userId; const monitors = await req.db.getMonitorsByUserId(userId); - logger.info(`Monitors for user ${userId} found`, { "service": "monitor", "userId":userId }); + logger.info(`Monitors for user ${userId} found`, { + service: "monitor", + userId: userId, + }); return res.json({ success: true, msg: `Monitors for user ${userId} found`, data: monitors, }); } catch (error) { - logger.error(error.message, { "service": "monitor" }); + logger.error(error.message, { service: "monitor" }); return res.status(500).json({ success: false, msg: error.message }); } }; -module.exports = { getAllMonitors, getMonitorById, getMonitorsByUserId }; +/** + * Creates a new monitor + * @async + * @param {Express.Request} req + * @param {Express.Response} res + * @returns {Promise} + * @throws {Error} + */ + +const createMonitor = async (req, res) => { + const { error } = monitorValidation.validate(req.body); + if (error) { + return res + .status(422) + .json({ success: false, msg: error.details[0].message }); + } + + try { + const monitor = await req.db.createMonitor(req, res); + return res + .status(201) + .json({ success: true, msg: "Monitor created", data: monitor }); + } catch (error) { + logger.error(error.message, { service: "monitor" }); + return res.status(500).json({ success: false, msg: error.message }); + } +}; + +/** + * Delete a monitor by ID + * @async + * @param {Express.Request} req + * @param {Express.Response} res + * @returns {Promise} + * @throws {Error} + */ + +const deleteMonitor = async (req, res) => { + const { error } = getMonitorByIdValidation.validate(req.params); + if (error) { + return res + .status(422) + .json({ success: false, msg: error.details[0].message }); + } + try { + const monitor = await req.db.deleteMonitor(req, res); + /** + * TODO + * We should remove all checks and alerts associated with this monitor + * when it is deleted so there is no orphaned data + * We also need to make sure to stop all running services for this monitor + */ + return res.status(204).json({ success: true, msg: "Monitor deleted" }); + } catch (error) { + logger.error(error.message, { service: "monitor" }); + return res.status(500).json({ success: false, msg: error.message }); + } +}; + +/** + * Edit a monitor by ID + * @async + * @param {Express.Request} req + * @param {Express.Response} res + * @returns {Promise} + * @throws {Error} + */ +const editMonitor = async (req, res) => { + let paramError = getMonitorByIdValidation.validate(req.params); + if (paramError.error) { + return res + .status(422) + .json({ success: false, msg: paramError.error.details[0].message }); + } + + let { error } = monitorValidation.validate(req.body); + if (error) { + return res + .status(422) + .json({ success: false, msg: error.details[0].message }); + } + + try { + const editedMonitor = await req.db.editMonitor(req, res); + return res + .status(200) + .json({ success: true, msg: "Monitor edited", data: editedMonitor }); + } catch (error) { + logger.error(error.message, { service: "monitor" }); + return res.status(500).json({ success: false, msg: error.message }); + } +}; + +module.exports = { + getAllMonitors, + getMonitorById, + getMonitorsByUserId, + createMonitor, + deleteMonitor, + editMonitor, +}; diff --git a/Server/db/FakeDb.js b/Server/db/FakeDb.js index 8cf05cf89..b2abd919e 100644 --- a/Server/db/FakeDb.js +++ b/Server/db/FakeDb.js @@ -22,25 +22,11 @@ const Monitor = require("../models/Monitor"); const UserModel = require("../models/user"); +const bcrypt = require("bcrypt"); -const FAKE_MONITOR_DATA = []; +let FAKE_MONITOR_DATA = []; const USERS = []; -for (let i = 0; i < 10; i++) { - FAKE_MONITOR_DATA.push( - new Monitor({ - userId: i % 2 === 0 ? 1 : 2, - name: `Monitor ${i}`, - description: `Description for Monitor ${i}`, - url: `https://monitor${i}.com`, - isActive: true, - interval: 60000, - updated_at: new Date(), - created_at: new Date(), - }) - ); -} - const connect = async () => { try { await console.log("Connected to FakeDB"); @@ -52,6 +38,8 @@ const connect = async () => { const insertUser = async (req, res) => { try { const newUser = new UserModel({ ...req.body }); + const salt = await bcrypt.genSalt(10); //genSalt is asynchronous, need to wait + newUser.password = await bcrypt.hash(newUser.password, salt); // hash is also async, need to eitehr await or use hashSync USERS.push(newUser); return newUser; } catch (error) { @@ -96,10 +84,45 @@ const getMonitorsByUserId = async (userId) => { if (userMonitors.length === 0) { throw new Error(`Monitors for user ${userId} not found`); } - return userMonitors; }; +const createMonitor = async (req, res) => { + const monitor = new Monitor(req.body); + monitor.createdAt = Date.now(); + monitor.updatedAt = Date.now(); + FAKE_MONITOR_DATA.push(monitor); + return monitor; +}; + +const deleteMonitor = async (req, res) => { + const monitorId = req.params.monitorId; + try { + const monitor = getMonitorById(monitorId); + FAKE_MONITOR_DATA = FAKE_MONITOR_DATA.filter((monitor) => { + return monitor.id !== monitorId; + }); + return monitor; + } catch (error) { + throw error; + } +}; + +const editMonitor = async (req, res) => { + const monitorId = req.params.monitorId; + const idx = FAKE_MONITOR_DATA.findIndex((monitor) => { + return monitor._id.toString() === monitorId; + }); + const oldMonitor = FAKE_MONITOR_DATA[idx]; + const editedMonitor = new Monitor({ ...req.body }); + editedMonitor._id = oldMonitor._id; + editedMonitor.userId = oldMonitor.userId; + editedMonitor.updatedAt = Date.now(); + editedMonitor.createdAt = oldMonitor.createdAt; + FAKE_MONITOR_DATA[idx] = editedMonitor; + return FAKE_MONITOR_DATA[idx]; +}; + module.exports = { connect, insertUser, @@ -107,4 +130,7 @@ module.exports = { getAllMonitors, getMonitorById, getMonitorsByUserId, + createMonitor, + deleteMonitor, + editMonitor, }; diff --git a/Server/db/MongoDB.js b/Server/db/MongoDB.js index e3edbb7e2..8fcc1dced 100644 --- a/Server/db/MongoDB.js +++ b/Server/db/MongoDB.js @@ -12,36 +12,70 @@ const connect = async () => { } }; +/** + * Insert a User + * @async + * @param {Express.Request} req + * @param {Express.Response} res + * @returns {Promise} + * @throws {Error} + */ const insertUser = async (req, res) => { try { - const newUser = await UserModel.create({ ...req.body }).select('-password'); - return newUser; + const newUser = new UserModel({ ...req.body }); + await newUser.save(); + return await UserModel.findOne({ _id: newUser._id }).select("-password"); // .select() doesn't work with create, need to save then find } catch (error) { throw error; } }; +/** + * Get User by Email + * Gets a user by Email. Not sure if we'll ever need this except for login. + * If not needed except for login, we can move password comparison here + * @async + * @param {Express.Request} req + * @param {Express.Response} res + * @returns {Promise} + * @throws {Error} + */ const getUserByEmail = async (req, res) => { try { // Returns null if no user is found - const user = await UserModel.findOne({ email: req.body.email }).select('-password'); - return user; + // Need the password to be able to compare, removed .select() + // We can strip the hash before returing the user + return await UserModel.findOne({ email: req.body.email }); } catch (error) { throw error; } }; -// Gets all monitors +/** + * Get all monitors + * @async + * @param {Express.Request} req + * @param {Express.Response} res + * @returns {Promise>} + * @throws {Error} + */ const getAllMonitors = async (req, res) => { try { - const monitors = await Monitor.find(); + const monitors = await Monitor.find({ isActive: true }); return monitors; } catch (error) { throw error; } }; -// Get a monitor by ID +/** + * Get a monitor by ID + * @async + * @param {Express.Request} req + * @param {Express.Response} res + * @returns {Promise} + * @throws {Error} + */ const getMonitorById = async (req, res) => { try { const monitor = await Monitor.findById(req.params.monitorId); @@ -51,7 +85,14 @@ const getMonitorById = async (req, res) => { } }; -// Gets a monitor by user ID +/** + * Get monitors by UserID + * @async + * @param {Express.Request} req + * @param {Express.Response} res + * @returns {Promise>} + * @throws {Error} + */ const getMonitorsByUserId = async (req, res) => { try { const monitors = await Monitor.find({ userId: req.params.userId }); @@ -61,6 +102,68 @@ const getMonitorsByUserId = async (req, res) => { } }; +/** + * Create a monitor + * @async + * @param {Express.Request} req + * @param {Express.Response} res + * @returns {Promise} + * @throws {Error} + */ +const createMonitor = async (req, res) => { + try { + const monitor = new Monitor({ ...req.body }); + await monitor.save(); + return monitor; + } catch (error) { + throw error; + } +}; + +/** + * Delete a monitor by ID + * @async + * @param {Express.Request} req + * @param {Express.Response} res + * @returns {Promise} + * @throws {Error} + */ +const deleteMonitor = async (req, res) => { + const monitorId = req.params.monitorId; + try { + const monitor = await Monitor.findByIdAndDelete(monitorId); + if (!monitor) { + throw new Error(`Monitor with id ${monitorId} not found`); + } + return monitor; + } catch (error) { + throw error; + } +}; + +/** + * Edit a monitor by ID + * @async + * @param {Express.Request} req + * @param {Express.Response} res + * @returns {Promise} + * @throws {Error} + */ +const editMonitor = async (req, res) => { + const candidateId = req.params.monitorId; + const candidateMonitor = req.body; + try { + const editedMonitor = await Monitor.findByIdAndUpdate( + candidateId, + candidateMonitor, + { new: true } + ); + return editedMonitor; + } catch (error) { + throw error; + } +}; + module.exports = { connect, insertUser, @@ -68,4 +171,7 @@ module.exports = { getAllMonitors, getMonitorById, getMonitorsByUserId, + createMonitor, + deleteMonitor, + editMonitor, }; diff --git a/Server/index.js b/Server/index.js index 7e68c72f2..4d2765cd8 100644 --- a/Server/index.js +++ b/Server/index.js @@ -17,10 +17,19 @@ const logger = require("./utils/logger"); // const db = require("./db/FakeDb"); // // MongoDB -const db = require("./db/MongoDB"); +// const db = require("./db/MongoDB"); // // ************************** +const DB_TYPE = { + MongoDB: () => require("./db/MongoDB"), + FakedDB: () => require("./db/FakeDb"), +}; + +const db = DB_TYPE[process.env.DB_TYPE] + ? DB_TYPE[process.env.DB_TYPE]() + : require("./db/FakeDb"); + /** * NOTES * Email Service will be added diff --git a/Server/models/Monitor.js b/Server/models/Monitor.js index 513d4e2be..4aaad1497 100644 --- a/Server/models/Monitor.js +++ b/Server/models/Monitor.js @@ -1,37 +1,36 @@ const mongoose = require("mongoose"); -const MonitorSchema = mongoose.Schema({ - userId: { - type: String, +const MonitorSchema = mongoose.Schema( + { + userId: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + immutable: true, + }, + name: { + type: String, + required: true, + }, + description: { + type: String, + }, + url: { + type: String, + required: true, + }, + isActive: { + type: Boolean, + default: true, + }, + interval: { + // in milliseconds + type: Number, + default: 60000, + }, }, - name: { - type: String, - required: true, - }, - description: { - type: String, - }, - url: { - type: String, - required: true, - }, - isActive: { - type: Boolean, - default: true, - }, - interval: { - // in milliseconds - type: Number, - default: 60000, - }, - updated_at: { - type: Date, - default: Date.now, - }, - created_at: { - type: Date, - default: Date.now, - }, -}); + { + timestamps: true, + } +); module.exports = mongoose.model("Monitor", MonitorSchema); diff --git a/Server/models/user.js b/Server/models/user.js index 822c303d8..02d0d53b3 100644 --- a/Server/models/user.js +++ b/Server/models/user.js @@ -45,7 +45,13 @@ UserSchema.pre("save", async function (next) { next(); } const salt = await bcrypt.genSalt(10); //genSalt is asynchronous, need to wait - this.password = bcrypt.hash(this.password, salt); + this.password = await bcrypt.hash(this.password, salt); // hash is also async, need to eitehr await or use hashSync + next(); }); +UserSchema.methods.comparePassword = function (submittedPassword) { + console.log(submittedPassword, this.password); + return bcrypt.compare(submittedPassword, this.password); +}; + module.exports = mongoose.model("User", UserSchema); diff --git a/Server/routes/authRoute.js b/Server/routes/authRoute.js index c778927e5..48a173b92 100644 --- a/Server/routes/authRoute.js +++ b/Server/routes/authRoute.js @@ -1,6 +1,10 @@ -const router = require('express').Router(); -const { registerController } = require('../controllers/authController') +const router = require("express").Router(); +const { + registerController, + loginController, +} = require("../controllers/authController"); -router.post('/register', registerController) +router.post("/register", registerController); +router.post("/login", loginController); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/Server/routes/monitorRoute.js b/Server/routes/monitorRoute.js index e12982905..5f2ce4bb5 100644 --- a/Server/routes/monitorRoute.js +++ b/Server/routes/monitorRoute.js @@ -1,7 +1,11 @@ const router = require("express").Router(); const monitorController = require("../controllers/monitorController"); + router.get("/", monitorController.getAllMonitors); router.get("/:monitorId", monitorController.getMonitorById); router.get("/user/:userId", monitorController.getMonitorsByUserId); +router.post("/", monitorController.createMonitor); +router.post("/delete/:monitorId", monitorController.deleteMonitor); +router.post("/edit/:monitorId", monitorController.editMonitor); module.exports = router; diff --git a/Server/validation/joi.js b/Server/validation/joi.js index 874daff93..ecc19253e 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -12,7 +12,7 @@ const registerValidation = joi.object({ password: joi.string().min(8).required(), }); -const getMonitorsByIdValidation = joi.object({ +const getMonitorByIdValidation = joi.object({ monitorId: joi.string().required(), }); @@ -20,9 +20,20 @@ const getMonitorsByUserIdValidation = joi.object({ userId: joi.string().required(), }); +const monitorValidation = joi.object({ + _id: joi.string(), + userId: joi.string().required(), + name: joi.string().required(), + description: joi.string().required(), + url: joi.string().uri().required(), + isActive: joi.boolean(), + interval: joi.number(), +}); + module.exports = { loginValidation, registerValidation, - getMonitorsByIdValidation, + getMonitorByIdValidation, getMonitorsByUserIdValidation, + monitorValidation, };