diff --git a/.gitignore b/.gitignore index 0be553d..cb33425 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,5 @@ next-env.d.ts # .env .env + +.github/* diff --git a/Dockerfile b/Dockerfile index 7a993dd..996cb80 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY src ./src COPY public ./public -COPY package.json next.config.js jsconfig.json ./ +COPY package.json next.config.mjs tsconfig.json ./ RUN npm run build # Stage 3: run diff --git a/next.config.mjs b/next.config.mjs index d6c1fa0..4339bc0 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,7 +1,7 @@ /** @type {import('next').NextConfig} */ const nextConfig = { env: { - BACKEND_URL: process.env.BACKEND_URL, + NEXT_PUBLIC_SERVER_URL: process.env.BACKEND_URL, }, }; diff --git a/package-lock.json b/package-lock.json index b1c7423..cda2437 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,19 +14,24 @@ "@fortawesome/free-brands-svg-icons": "^6.6.0", "@fortawesome/free-solid-svg-icons": "^6.5.2", "@fortawesome/react-fontawesome": "^0.2.2", + "@hookform/resolvers": "^3.9.0", "@mui/icons-material": "^5.16.7", + "@mui/lab": "^5.0.0-alpha.173", "@mui/material": "^5.16.7", + "@tanstack/react-query": "^5.51.23", "axios": "^1.7.2", "bootstrap": "^5.3.3", "cookie": "^0.6.0", "cookies-next": "^4.2.1", "file-saver": "^2.0.5", "font-awesome": "^4.7.0", + "html-react-parser": "^5.1.15", "html-to-text": "^9.0.5", "html2canvas": "^1.4.1", "html2pdf.js": "^0.10.2", "jspdf": "^2.5.1", - "next": "^14.2.3", + "lodash": "^4.17.21", + "next": "14.2.3", "next-cookie": "^2.8.0", "pdf-lib": "^1.17.1", "quill": "^2.0.2", @@ -35,16 +40,21 @@ "react": "^18.3.1", "react-bootstrap": "^2.10.4", "react-dom": "^18", + "react-helmet-async": "^2.0.5", + "react-hook-form": "^7.52.2", + "react-mui-sidebar": "^1.3.8", "react-quill": "^2.0.0", "react-router-dom": "^6.26.0", "react-toastify": "^10.0.5", - "socket.io-client": "^4.7.5" + "socket.io-client": "^4.7.5", + "zod": "^3.23.8" }, "devDependencies": { "@types/file-saver": "^2.0.7", "@types/html-to-text": "^9.0.4", - "@types/node": "^20.14.14", - "@types/react": "^18.3.3", + "@types/lodash": "^4.17.7", + "@types/node": "^20.3.1", + "@types/react": "^18", "@types/react-dom": "^18", "eslint": "^8", "eslint-config-next": "14.2.3", @@ -407,6 +417,374 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "aix" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -463,6 +841,40 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.7.tgz", + "integrity": "sha512-yDzVT/Lm101nQ5TCVeK65LtdN7Tj4Qpr9RTXJ2vPFLqtLxwOrpoxAHAJI8J3yYWUc40J0BDBheaitK5SJmno2g==", + "dependencies": { + "@floating-ui/utils": "^0.2.7" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.10", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.10.tgz", + "integrity": "sha512-fskgCFv8J8OamCmyun8MfjB1Olfn+uZKjOKZ0vhYF3gRmEUXcGOjxWL8bBr7i4kIuPZ2KD2S3EUIOxnjC8kl2A==", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.7" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.1.tgz", + "integrity": "sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg==", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.7.tgz", + "integrity": "sha512-X8R8Oj771YRl/w+c1HqAC1szL8zWQRwFvgDwT129k9ACdBoud/+/rX9V0qiMl6LWUdP9voC2nDVZYPMQQsb6eA==" + }, "node_modules/@fortawesome/fontawesome-common-types": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.2.tgz", @@ -527,6 +939,14 @@ "react": ">=16.3" } }, + "node_modules/@hookform/resolvers": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.0.tgz", + "integrity": "sha512-bU0Gr4EepJ/EQsH/IwEzYLsT/PEj5C0ynLQ4m+GSHS+xKH4TfSelhluTgOaoc4kA5s7eCsQbM4wvZLzELmWzUg==", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -634,9 +1054,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", @@ -647,9 +1067,167 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@mui/core-downloads-tracker": { - "version": "5.16.7", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.7.tgz", + "node_modules/@microsoft/api-extractor": { + "version": "7.43.0", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.43.0.tgz", + "integrity": "sha512-GFhTcJpB+MI6FhvXEI9b2K0snulNLWHqC/BbcJtyNYcKUiw7l3Lgis5ApsYncJ0leALX7/of4XfmXk+maT111w==", + "peer": true, + "dependencies": { + "@microsoft/api-extractor-model": "7.28.13", + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "~0.16.1", + "@rushstack/node-core-library": "4.0.2", + "@rushstack/rig-package": "0.5.2", + "@rushstack/terminal": "0.10.0", + "@rushstack/ts-command-line": "4.19.1", + "lodash": "~4.17.15", + "minimatch": "~3.0.3", + "resolve": "~1.22.1", + "semver": "~7.5.4", + "source-map": "~0.6.1", + "typescript": "5.4.2" + }, + "bin": { + "api-extractor": "bin/api-extractor" + } + }, + "node_modules/@microsoft/api-extractor-model": { + "version": "7.28.13", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.28.13.tgz", + "integrity": "sha512-39v/JyldX4MS9uzHcdfmjjfS6cYGAoXV+io8B5a338pkHiSt+gy2eXQ0Q7cGFJ7quSa1VqqlMdlPrB6sLR/cAw==", + "peer": true, + "dependencies": { + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "~0.16.1", + "@rushstack/node-core-library": "4.0.2" + } + }, + "node_modules/@microsoft/api-extractor/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "peer": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@microsoft/api-extractor/node_modules/minimatch": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", + "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@microsoft/api-extractor/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "peer": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@microsoft/api-extractor/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@microsoft/api-extractor/node_modules/typescript": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", + "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@microsoft/tsdoc": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz", + "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==", + "peer": true + }, + "node_modules/@microsoft/tsdoc-config": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.16.2.tgz", + "integrity": "sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==", + "peer": true, + "dependencies": { + "@microsoft/tsdoc": "0.14.2", + "ajv": "~6.12.6", + "jju": "~1.4.0", + "resolve": "~1.19.0" + } + }, + "node_modules/@microsoft/tsdoc-config/node_modules/resolve": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", + "peer": true, + "dependencies": { + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@mui/base": { + "version": "5.0.0-beta.40", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz", + "integrity": "sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@floating-ui/react-dom": "^2.0.8", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.14", + "@popperjs/core": "^2.11.8", + "clsx": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "5.16.7", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.7.tgz", "integrity": "sha512-RtsCt4Geed2/v74sbihWzzRs+HsIQCfclHeORh5Ynu2fS4icIKozcSubwuG7vtzq2uW3fOR1zITSP84TNt2GoQ==", "funding": { "type": "opencollective", @@ -681,6 +1259,46 @@ } } }, + "node_modules/@mui/lab": { + "version": "5.0.0-alpha.173", + "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.173.tgz", + "integrity": "sha512-Gt5zopIWwxDgGy/MXcp6GueD84xFFugFai4hYiXY0zowJpTVnIrTQCQXV004Q7rejJ7aaCntX9hpPJqCrioshA==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/base": "5.0.0-beta.40", + "@mui/system": "^5.16.5", + "@mui/types": "^7.2.15", + "@mui/utils": "^5.16.5", + "clsx": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material": ">=5.15.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, "node_modules/@mui/material": { "version": "5.16.7", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.7.tgz", @@ -1153,91 +1771,683 @@ "react": ">=16.14.0" } }, - "node_modules/@rushstack/eslint-patch": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz", - "integrity": "sha512-qC/xYId4NMebE6w/V33Fh9gWxLgURiNYgVNObbJl2LZv0GUUItCcCqC5axQSwRaAgaxl2mELq1rMzlswaQ0Zxg==", - "dev": true - }, - "node_modules/@selderee/plugin-htmlparser2": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", - "integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==", + "node_modules/@rollup/pluginutils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", + "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", + "peer": true, "dependencies": { - "domhandler": "^5.0.3", - "selderee": "^0.11.0" + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" }, - "funding": { - "url": "https://ko-fi.com/killymxi" + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } } }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", - "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.20.0.tgz", + "integrity": "sha512-TSpWzflCc4VGAUJZlPpgAJE1+V60MePDQnBd7PPkpuEmOy8i87aL6tinFGKBFKuEDikYpig72QzdT3QPYIi+oA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "peer": true }, - "node_modules/@swc/counter": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==" + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.20.0.tgz", + "integrity": "sha512-u00Ro/nok7oGzVuh/FMYfNoGqxU5CPWz1mxV85S2w9LxHR8OoMQBuSk+3BKVIDYgkpeOET5yXkx90OYFc+ytpQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "peer": true }, - "node_modules/@swc/helpers": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", - "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", - "dependencies": { - "@swc/counter": "^0.1.3", - "tslib": "^2.4.0" - } + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.20.0.tgz", + "integrity": "sha512-uFVfvzvsdGtlSLuL0ZlvPJvl6ZmrH4CBwLGEFPe7hUmf7htGAN+aXo43R/V6LATyxlKVC/m6UsLb7jbG+LG39Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "peer": true }, - "node_modules/@types/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", - "license": "MIT" + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.20.0.tgz", + "integrity": "sha512-xbrMDdlev53vNXexEa6l0LffojxhqDTBeL+VUxuuIXys4x6xyvbKq5XqTXBCEUA8ty8iEJblHvFaWRJTk/icAQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "peer": true }, - "node_modules/@types/file-saver": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz", - "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==", - "dev": true + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.20.0.tgz", + "integrity": "sha512-jMYvxZwGmoHFBTbr12Xc6wOdc2xA5tF5F2q6t7Rcfab68TT0n+r7dgawD4qhPEvasDsVpQi+MgDzj2faOLsZjA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true }, - "node_modules/@types/html-to-text": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/@types/html-to-text/-/html-to-text-9.0.4.tgz", - "integrity": "sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ==", - "dev": true + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.20.0.tgz", + "integrity": "sha512-1asSTl4HKuIHIB1GcdFHNNZhxAYEdqML/MW4QmPS4G0ivbEcBr1JKlFLKsIRqjSwOBkdItn3/ZDlyvZ/N6KPlw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.20.0.tgz", + "integrity": "sha512-COBb8Bkx56KldOYJfMf6wKeYJrtJ9vEgBRAOkfw6Ens0tnmzPqvlpjZiLgkhg6cA3DGzCmLmmd319pmHvKWWlQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true }, - "node_modules/@types/node": { - "version": "20.14.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.14.tgz", - "integrity": "sha512-d64f00982fS9YoOgJkAMolK7MN8Iq3TDdVjchbYHdEmjth/DHowx82GnoA+tVUAN+7vxfYUgAzi+JXbKNd2SDQ==", - "dev": true, - "dependencies": { - "undici-types": "~5.26.4" - } + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.20.0.tgz", + "integrity": "sha512-+it+mBSyMslVQa8wSPvBx53fYuZK/oLTu5RJoXogjk6x7Q7sz1GNRsXWjn6SwyJm8E/oMjNVwPhmNdIjwP135Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true }, - "node_modules/@types/parse-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.20.0.tgz", + "integrity": "sha512-yAMvqhPfGKsAxHN8I4+jE0CpLWD8cv4z7CK7BMmhjDuz606Q2tFKkWRY8bHR9JQXYcoLfopo5TTqzxgPUjUMfw==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true }, - "node_modules/@types/prop-types": { - "version": "15.7.12", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", - "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.20.0.tgz", + "integrity": "sha512-qmuxFpfmi/2SUkAw95TtNq/w/I7Gpjurx609OOOV7U4vhvUhBcftcmXwl3rqAek+ADBwSjIC4IVNLiszoj3dPA==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true }, - "node_modules/@types/quill": { - "version": "1.3.10", - "resolved": "https://registry.npmjs.org/@types/quill/-/quill-1.3.10.tgz", - "integrity": "sha512-IhW3fPW+bkt9MLNlycw8u8fWb7oO7W5URC9MfZYHBlA24rex9rs23D5DETChu1zvgVdc5ka64ICjJOgQMr6Shw==", - "dependencies": { + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.20.0.tgz", + "integrity": "sha512-I0BtGXddHSHjV1mqTNkgUZLnS3WtsqebAXv11D5BZE/gfw5KoyXSAXVqyJximQXNvNzUo4GKlCK/dIwXlz+jlg==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.20.0.tgz", + "integrity": "sha512-y+eoL2I3iphUg9tN9GB6ku1FA8kOfmF4oUEWhztDJ4KXJy1agk/9+pejOuZkNFhRwHAOxMsBPLbXPd6mJiCwew==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.20.0.tgz", + "integrity": "sha512-hM3nhW40kBNYUkZb/r9k2FKK+/MnKglX7UYd4ZUy5DJs8/sMsIbqWK2piZtVGE3kcXVNj3B2IrUYROJMMCikNg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.20.0.tgz", + "integrity": "sha512-psegMvP+Ik/Bg7QRJbv8w8PAytPA7Uo8fpFjXyCRHWm6Nt42L+JtoqH8eDQ5hRP7/XW2UiIriy1Z46jf0Oa1kA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.20.0.tgz", + "integrity": "sha512-GabekH3w4lgAJpVxkk7hUzUf2hICSQO0a/BLFA11/RMxQT92MabKAqyubzDZmMOC/hcJNlc+rrypzNzYl4Dx7A==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.20.0.tgz", + "integrity": "sha512-aJ1EJSuTdGnM6qbVC4B5DSmozPTqIag9fSzXRNNo+humQLG89XpPgdt16Ia56ORD7s+H8Pmyx44uczDQ0yDzpg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz", + "integrity": "sha512-qC/xYId4NMebE6w/V33Fh9gWxLgURiNYgVNObbJl2LZv0GUUItCcCqC5axQSwRaAgaxl2mELq1rMzlswaQ0Zxg==", + "dev": true + }, + "node_modules/@rushstack/node-core-library": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-4.0.2.tgz", + "integrity": "sha512-hyES82QVpkfQMeBMteQUnrhASL/KHPhd7iJ8euduwNJG4mu2GSOKybf0rOEjOm1Wz7CwJEUm9y0yD7jg2C1bfg==", + "peer": true, + "dependencies": { + "fs-extra": "~7.0.1", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.22.1", + "semver": "~7.5.4", + "z-schema": "~5.0.2" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@rushstack/node-core-library/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "peer": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "peer": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@rushstack/rig-package": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.5.2.tgz", + "integrity": "sha512-mUDecIJeH3yYGZs2a48k+pbhM6JYwWlgjs2Ca5f2n1G2/kgdgP9D/07oglEGf6mRyXEnazhEENeYTSNDRCwdqA==", + "peer": true, + "dependencies": { + "resolve": "~1.22.1", + "strip-json-comments": "~3.1.1" + } + }, + "node_modules/@rushstack/terminal": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.10.0.tgz", + "integrity": "sha512-UbELbXnUdc7EKwfH2sb8ChqNgapUOdqcCIdQP4NGxBpTZV2sQyeekuK3zmfQSa/MN+/7b4kBogl2wq0vpkpYGw==", + "peer": true, + "dependencies": { + "@rushstack/node-core-library": "4.0.2", + "supports-color": "~8.1.1" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@rushstack/terminal/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/@rushstack/ts-command-line": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.19.1.tgz", + "integrity": "sha512-J7H768dgcpG60d7skZ5uSSwyCZs/S2HrWP1Ds8d1qYAyaaeJmpmmLr9BVw97RjFzmQPOYnoXcKA4GkqDCkduQg==", + "peer": true, + "dependencies": { + "@rushstack/terminal": "0.10.0", + "@types/argparse": "1.0.38", + "argparse": "~1.0.9", + "string-argv": "~0.3.1" + } + }, + "node_modules/@rushstack/ts-command-line/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "peer": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@selderee/plugin-htmlparser2": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", + "integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==", + "dependencies": { + "domhandler": "^5.0.3", + "selderee": "^0.11.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" + }, + "node_modules/@swc/core": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.11.tgz", + "integrity": "sha512-AB+qc45UrJrDfbhPKcUXk+9z/NmFfYYwJT6G7/iur0fCse9kXjx45gi40+u/O2zgarG/30/zV6E3ps8fUvjh7g==", + "hasInstallScript": true, + "peer": true, + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.12" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.7.11", + "@swc/core-darwin-x64": "1.7.11", + "@swc/core-linux-arm-gnueabihf": "1.7.11", + "@swc/core-linux-arm64-gnu": "1.7.11", + "@swc/core-linux-arm64-musl": "1.7.11", + "@swc/core-linux-x64-gnu": "1.7.11", + "@swc/core-linux-x64-musl": "1.7.11", + "@swc/core-win32-arm64-msvc": "1.7.11", + "@swc/core-win32-ia32-msvc": "1.7.11", + "@swc/core-win32-x64-msvc": "1.7.11" + }, + "peerDependencies": { + "@swc/helpers": "*" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.11.tgz", + "integrity": "sha512-HRQv4qIeMBPThZ6Y/4yYW52rGsS6yrpusvuxLGyoFo45Y0y12/V2yXkOIA/0HIQyrqoUAxn1k4zQXpPaPNCmnw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.11.tgz", + "integrity": "sha512-vtMQj0F3oYwDu5yhO7SKDRg1XekRSi6/TbzHAbBXv+dBhlGGvcZZynT1H90EVFTv+7w7Sh+lOFvRv5Z4ZTcxow==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.11.tgz", + "integrity": "sha512-mHtzWKxhtyreI4CSxs+3+ENv8t/Qo35WFoYG66qHEgJz/Z2Lh6jv1E+MYgHdYwnpQHgHbdvAco7HsBu/Dt6xXw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.11.tgz", + "integrity": "sha512-FRwe/x0GfXSQjGP2lIk+NO0pUFS/lI/RorCLBPiK808EVE9JTbh9DKCc/4Bbb4jgScAjNkrFCUVObQYl3YKmpA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.11.tgz", + "integrity": "sha512-GY/rs0+GUq14Gbnza90KOrQd/9yHd5qQMii5jcSWcUCT5A8QTa8kiicsM2NxZeTJ69xlKmT7sLod5l99lki/2A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.11.tgz", + "integrity": "sha512-QDkGRwSPmp2RBOlSs503IUXlWYlny8DyznTT0QuK0ML2RpDFlXWU94K/EZhS0RBEUkMY/W51OacM8P8aS/dkCg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.11.tgz", + "integrity": "sha512-SBEfKrXy6zQ6ksnyxw1FaCftrIH4fLfA81xNnKb7x/6iblv7Ko6H0aK3P5C86jyqF/82+ONl9C7ImGkUFQADig==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.11.tgz", + "integrity": "sha512-a2Y4xxEsLLYHJN7sMnw9+YQJDi3M1BxEr9hklfopPuGGnYLFNnx5CypH1l9ReijEfWjIAHNi7pq3m023lzW1Hg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.11.tgz", + "integrity": "sha512-ZbZFMwZO+j8ulhegJ7EhJ/QVZPoQ5qc30ylJQSxizizTJaen71Q7/13lXWc6ksuCKvg6dUKrp/TPgoxOOtSrFA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.11.tgz", + "integrity": "sha512-IUohZedSJyDu/ReEBG/mqX6uG29uA7zZ9z6dIAF+p6eFxjXmh9MuHryyM+H8ebUyoq/Ad3rL+rUCksnuYNnI0w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==" + }, + "node_modules/@swc/helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", + "dependencies": { + "@swc/counter": "^0.1.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@swc/types": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.12.tgz", + "integrity": "sha512-wBJA+SdtkbFhHjTMYH+dEH1y4VpfGdAc2Kw/LK09i9bXd/K6j6PkDcFCEzb6iVfZMkPRrl/q0e3toqTAJdkIVA==", + "peer": true, + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.51.21", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.51.21.tgz", + "integrity": "sha512-POQxm42IUp6n89kKWF4IZi18v3fxQWFRolvBA6phNVmA8psdfB1MvDnGacCJdS+EOX12w/CyHM62z//rHmYmvw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.51.23", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.51.23.tgz", + "integrity": "sha512-CfJCfX45nnVIZjQBRYYtvVMIsGgWLKLYC4xcUiYEey671n1alvTZoCBaU9B85O8mF/tx9LPyrI04A6Bs2THv4A==", + "dependencies": { + "@tanstack/query-core": "5.51.21" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, + "node_modules/@types/argparse": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", + "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==", + "peer": true + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "peer": true + }, + "node_modules/@types/file-saver": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz", + "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==", + "dev": true + }, + "node_modules/@types/html-to-text": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@types/html-to-text/-/html-to-text-9.0.4.tgz", + "integrity": "sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ==", + "dev": true + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/@types/lodash": { + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", + "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.14.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.14.tgz", + "integrity": "sha512-d64f00982fS9YoOgJkAMolK7MN8Iq3TDdVjchbYHdEmjth/DHowx82GnoA+tVUAN+7vxfYUgAzi+JXbKNd2SDQ==", + "devOptional": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" + }, + "node_modules/@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" + }, + "node_modules/@types/quill": { + "version": "1.3.10", + "resolved": "https://registry.npmjs.org/@types/quill/-/quill-1.3.10.tgz", + "integrity": "sha512-IhW3fPW+bkt9MLNlycw8u8fWb7oO7W5URC9MfZYHBlA24rex9rs23D5DETChu1zvgVdc5ka64ICjJOgQMr6Shw==", + "dependencies": { "parchment": "^1.1.2" } }, @@ -1369,20 +2579,155 @@ } } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.2.0.tgz", + "integrity": "sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.2.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/@vitejs/plugin-react-swc": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.7.0.tgz", + "integrity": "sha512-yrknSb3Dci6svCd/qhHqhFPDSw0QtjumcqdKMoNNzmOl5lMXTTiqzjWtG4Qask2HdvvzaNgSunbQGet8/GrKdA==", + "peer": true, + "dependencies": { + "@swc/core": "^1.5.7" + }, + "peerDependencies": { + "vite": "^4 || ^5" + } + }, + "node_modules/@volar/language-core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.11.1.tgz", + "integrity": "sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==", + "peer": true, + "dependencies": { + "@volar/source-map": "1.11.1" + } + }, + "node_modules/@volar/source-map": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.11.1.tgz", + "integrity": "sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==", + "peer": true, + "dependencies": { + "muggle-string": "^0.3.1" + } + }, + "node_modules/@volar/typescript": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.11.1.tgz", + "integrity": "sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==", + "peer": true, + "dependencies": { + "@volar/language-core": "1.11.1", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.38.tgz", + "integrity": "sha512-8IQOTCWnLFqfHzOGm9+P8OPSEDukgg3Huc92qSG49if/xI2SAwLHQO2qaPQbjCWPBcQoO1WYfXfTACUrWV3c5A==", + "peer": true, + "dependencies": { + "@babel/parser": "^7.24.7", + "@vue/shared": "3.4.38", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.38.tgz", + "integrity": "sha512-Osc/c7ABsHXTsETLgykcOwIxFktHfGSUDkb05V61rocEfsFDcjDLH/IHJSNJP+/Sv9KeN2Lx1V6McZzlSb9EhQ==", + "peer": true, + "dependencies": { + "@vue/compiler-core": "3.4.38", + "@vue/shared": "3.4.38" + } + }, + "node_modules/@vue/language-core": { + "version": "1.8.27", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.27.tgz", + "integrity": "sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==", + "peer": true, + "dependencies": { + "@volar/language-core": "~1.11.1", + "@volar/source-map": "~1.11.1", + "@vue/compiler-dom": "^3.3.0", + "@vue/shared": "^3.3.0", + "computeds": "^0.0.1", + "minimatch": "^9.0.3", + "muggle-string": "^0.3.1", + "path-browserify": "^1.0.1", + "vue-template-compiler": "^2.7.14" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/language-core/node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, + "peer": true, "dependencies": { "balanced-match": "^1.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, + "node_modules/@vue/language-core/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "peer": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -1393,28 +2738,11 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.2.0.tgz", - "integrity": "sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "7.2.0", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true + "node_modules/@vue/shared": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.38.tgz", + "integrity": "sha512-q0xCiLkuWWQLzVrecPb0RMsNWyxICOjPrcrwxTUEHb1fsnvni4dcuyG7RT/Ie7VPTvnjzIaWzRMUBsrqNj/hhw==", + "peer": true }, "node_modules/acorn": { "version": "8.11.3", @@ -1441,7 +2769,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1767,8 +3094,7 @@ "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/base64-arraybuffer": { "version": "1.0.2", @@ -1812,7 +3138,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" @@ -2048,11 +3373,16 @@ "node": ">= 6" } }, + "node_modules/computeds": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz", + "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==", + "peer": true + }, "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/convert-source-map": { "version": "1.9.0", @@ -2208,6 +3538,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "peer": true + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2632,6 +3968,44 @@ "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "hasInstallScript": true, + "peer": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -3046,6 +4420,12 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "peer": true + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -3068,8 +4448,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-diff": { "version": "1.3.0", @@ -3107,8 +4486,7 @@ "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "node_modules/fast-levenshtein": { "version": "2.0.6", @@ -3265,6 +4643,20 @@ "node": ">= 6" } }, + "node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3275,7 +4667,6 @@ "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": [ @@ -3510,7 +4901,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -3573,6 +4963,15 @@ "node": ">= 0.4" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "peer": true, + "bin": { + "he": "bin/he" + } + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -3581,6 +4980,56 @@ "react-is": "^16.7.0" } }, + "node_modules/html-dom-parser": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/html-dom-parser/-/html-dom-parser-5.0.10.tgz", + "integrity": "sha512-GwArYL3V3V8yU/mLKoFF7HlLBv80BZ2Ey1BzfVNRpAci0cEKhFHI/Qh8o8oyt3qlAMLlK250wsxLdYX4viedvg==", + "license": "MIT", + "dependencies": { + "domhandler": "5.0.3", + "htmlparser2": "9.1.0" + } + }, + "node_modules/html-dom-parser/node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "node_modules/html-react-parser": { + "version": "5.1.15", + "resolved": "https://registry.npmjs.org/html-react-parser/-/html-react-parser-5.1.15.tgz", + "integrity": "sha512-LRwSTseAZtdtzYbBaN0a+pJ48x4qmwPzQC5tvwAp9IvuNf7afxtTHLpCPYCsVjRKRUqhXvfjTaKJJrhctxkHJA==", + "license": "MIT", + "dependencies": { + "domhandler": "5.0.3", + "html-dom-parser": "5.0.10", + "react-property": "2.0.2", + "style-to-js": "1.1.13" + }, + "peerDependencies": { + "@types/react": "0.14 || 15 || 16 || 17 || 18", + "react": "0.14 || 15 || 16 || 17 || 18" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/html-to-text": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz", @@ -3666,6 +5115,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -3692,6 +5150,12 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "node_modules/inline-style-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.3.tgz", + "integrity": "sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g==", + "license": "MIT" + }, "node_modules/internal-slot": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", @@ -4150,6 +5614,12 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "peer": true + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4192,8 +5662,7 @@ "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -4213,6 +5682,15 @@ "json5": "lib/cli.js" } }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "peer": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/jspdf": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.1.tgz", @@ -4254,6 +5732,12 @@ "json-buffer": "3.0.1" } }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "peer": true + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -4337,6 +5821,12 @@ "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "peer": true + }, "node_modules/lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", @@ -4368,6 +5858,15 @@ "node": "14 || >=16.14" } }, + "node_modules/magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "peer": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -4444,6 +5943,12 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/muggle-string": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.3.1.tgz", + "integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==", + "peer": true + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -4828,6 +6333,12 @@ "url": "https://ko-fi.com/killymxi" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "peer": true + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4923,7 +6434,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "devOptional": true, "engines": { "node": ">=8.6" }, @@ -4959,10 +6469,9 @@ } }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", - "dev": true, + "version": "8.4.41", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", + "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", "funding": [ { "type": "opencollective", @@ -4979,7 +6488,7 @@ ], "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", + "picocolors": "^1.0.1", "source-map-js": "^1.2.0" }, "engines": { @@ -5147,7 +6656,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "engines": { "node": ">=6" } @@ -5329,6 +6837,39 @@ "react": "^18.3.1" } }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" + }, + "node_modules/react-helmet-async": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/react-helmet-async/-/react-helmet-async-2.0.5.tgz", + "integrity": "sha512-rYUYHeus+i27MvFE+Jaa4WsyBKGkL6qVgbJvSBoX8mbsWoABJXdEO0bZyi0F6i+4f0NuIb8AvqPMj3iXFHkMwg==", + "dependencies": { + "invariant": "^2.2.4", + "react-fast-compare": "^3.2.2", + "shallowequal": "^1.1.0" + }, + "peerDependencies": { + "react": "^16.6.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-hook-form": { + "version": "7.52.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.52.2.tgz", + "integrity": "sha512-pqfPEbERnxxiNMPd0bzmt1tuaPcVccywFDpyk2uV5xCIBphHV5T8SVnX9/o3kplPE1zzKt77+YIoq+EMwJp56A==", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -5339,6 +6880,23 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "node_modules/react-mui-sidebar": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/react-mui-sidebar/-/react-mui-sidebar-1.3.8.tgz", + "integrity": "sha512-/EU293TQ2WsPFP1ecf3VBC346phkVKo7VGizVgL8agvlJV6U9UXBvWdAL4vIf4EzyPTmo11Plp7BzvmFB+6KoQ==", + "peerDependencies": { + "@vitejs/plugin-react-swc": "^3.6.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "vite-plugin-dts": "^3.8.1" + } + }, + "node_modules/react-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/react-property/-/react-property-2.0.2.tgz", + "integrity": "sha512-+PbtI3VuDV0l6CleQMsx2gtK0JZbZKbpdu5ynr+lbsuvtmgbNcS3VM0tuY2QjFNOcWxvXeHjDpy42RO+4U2rug==", + "license": "MIT" + }, "node_modules/react-quill": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/react-quill/-/react-quill-2.0.0.tgz", @@ -5603,6 +7161,41 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rollup": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.20.0.tgz", + "integrity": "sha512-6rbWBChcnSGzIlXeIdNIZTopKYad8ZG8ajhl78lGRLsI2rX8IkaotQhVas2Ma+GPxJav19wrSzvRvuiv0YKzWw==", + "peer": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.20.0", + "@rollup/rollup-android-arm64": "4.20.0", + "@rollup/rollup-darwin-arm64": "4.20.0", + "@rollup/rollup-darwin-x64": "4.20.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.20.0", + "@rollup/rollup-linux-arm-musleabihf": "4.20.0", + "@rollup/rollup-linux-arm64-gnu": "4.20.0", + "@rollup/rollup-linux-arm64-musl": "4.20.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.20.0", + "@rollup/rollup-linux-riscv64-gnu": "4.20.0", + "@rollup/rollup-linux-s390x-gnu": "4.20.0", + "@rollup/rollup-linux-x64-gnu": "4.20.0", + "@rollup/rollup-linux-x64-musl": "4.20.0", + "@rollup/rollup-win32-arm64-msvc": "4.20.0", + "@rollup/rollup-win32-ia32-msvc": "4.20.0", + "@rollup/rollup-win32-x64-msvc": "4.20.0", + "fsevents": "~2.3.2" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5701,7 +7294,6 @@ "version": "7.6.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "dev": true, "bin": { "semver": "bin/semver.js" }, @@ -5739,6 +7331,11 @@ "node": ">= 0.4" } }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5841,6 +7438,12 @@ "node": ">=0.10.0" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "peer": true + }, "node_modules/stackblur-canvas": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", @@ -5858,6 +7461,15 @@ "node": ">=10.0.0" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "peer": true, + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -6036,7 +7648,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, "engines": { "node": ">=8" }, @@ -6044,6 +7655,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-to-js": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.13.tgz", + "integrity": "sha512-+43kvxwjrW9n5gFR40Rv98A0/Mcjew7Lt+p5Nnw1KGR9SZf/ZaKqmMwl9Enj9EnYNcJ5VzuCjejC5KZzvH2lOA==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.6" + } + }, + "node_modules/style-to-object": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.6.tgz", + "integrity": "sha512-khxq+Qm3xEyZfKd/y9L3oIWQimxuc4STrQKtQn8aSDRHb8mFgpukgX1hdzfrMEW6JCjyJ8p89x+IUMVnCBI1PA==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.3" + } + }, "node_modules/styled-jsx": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", @@ -6362,7 +7991,6 @@ "version": "5.5.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", - "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6404,7 +8032,7 @@ "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "devOptional": true }, "node_modules/universal-cookie": { "version": "4.0.4", @@ -6431,11 +8059,19 @@ "node": ">= 0.6" } }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "peer": true, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "dependencies": { "punycode": "^2.1.0" } @@ -6454,6 +8090,128 @@ "base64-arraybuffer": "^1.0.2" } }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "peer": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vite": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.1.tgz", + "integrity": "sha512-1oE6yuNXssjrZdblI9AfBbHCC41nnyoVoEZxQnID6yvQZAFBzxxkqoFLtHUMkYunL8hwOLEjgTuxpkRxvba3kA==", + "peer": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.41", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-plugin-dts": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/vite-plugin-dts/-/vite-plugin-dts-3.9.1.tgz", + "integrity": "sha512-rVp2KM9Ue22NGWB8dNtWEr+KekN3rIgz1tWD050QnRGlriUCmaDwa7qA5zDEjbXg5lAXhYMSBJtx3q3hQIJZSg==", + "peer": true, + "dependencies": { + "@microsoft/api-extractor": "7.43.0", + "@rollup/pluginutils": "^5.1.0", + "@vue/language-core": "^1.8.27", + "debug": "^4.3.4", + "kolorist": "^1.8.0", + "magic-string": "^0.30.8", + "vue-tsc": "^1.8.27" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "typescript": "*", + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vue-template-compiler": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", + "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", + "peer": true, + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/vue-tsc": { + "version": "1.8.27", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.8.27.tgz", + "integrity": "sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==", + "peer": true, + "dependencies": { + "@volar/typescript": "~1.11.1", + "@vue/language-core": "1.8.27", + "semver": "^7.5.4" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": "*" + } + }, "node_modules/warning": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", @@ -6693,6 +8451,12 @@ "node": ">=0.4.0" } }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "peer": true + }, "node_modules/yaml": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz", @@ -6716,6 +8480,45 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "peer": true, + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + }, + "node_modules/z-schema/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "optional": true, + "peer": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 813ec38..379801d 100644 --- a/package.json +++ b/package.json @@ -15,19 +15,24 @@ "@fortawesome/free-brands-svg-icons": "^6.6.0", "@fortawesome/free-solid-svg-icons": "^6.5.2", "@fortawesome/react-fontawesome": "^0.2.2", + "@hookform/resolvers": "^3.9.0", "@mui/icons-material": "^5.16.7", + "@mui/lab": "^5.0.0-alpha.173", "@mui/material": "^5.16.7", + "@tanstack/react-query": "^5.51.23", "axios": "^1.7.2", "bootstrap": "^5.3.3", "cookie": "^0.6.0", "cookies-next": "^4.2.1", "file-saver": "^2.0.5", "font-awesome": "^4.7.0", + "html-react-parser": "^5.1.15", "html-to-text": "^9.0.5", "html2canvas": "^1.4.1", "html2pdf.js": "^0.10.2", "jspdf": "^2.5.1", - "next": "^14.2.3", + "lodash": "^4.17.21", + "next": "14.2.3", "next-cookie": "^2.8.0", "pdf-lib": "^1.17.1", "quill": "^2.0.2", @@ -36,16 +41,21 @@ "react": "^18.3.1", "react-bootstrap": "^2.10.4", "react-dom": "^18", + "react-helmet-async": "^2.0.5", + "react-hook-form": "^7.52.2", + "react-mui-sidebar": "^1.3.8", "react-quill": "^2.0.0", "react-router-dom": "^6.26.0", "react-toastify": "^10.0.5", - "socket.io-client": "^4.7.5" + "socket.io-client": "^4.7.5", + "zod": "^3.23.8" }, "devDependencies": { "@types/file-saver": "^2.0.7", "@types/html-to-text": "^9.0.4", - "@types/node": "^20.14.14", - "@types/react": "^18.3.3", + "@types/lodash": "^4.17.7", + "@types/node": "^20.3.1", + "@types/react": "^18", "@types/react-dom": "^18", "eslint": "^8", "eslint-config-next": "14.2.3", diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 0000000..553200f Binary files /dev/null and b/public/logo.png differ diff --git a/public/user.png b/public/user.png new file mode 100644 index 0000000..14ed98e Binary files /dev/null and b/public/user.png differ diff --git a/src/api/auth.client.ts b/src/api/auth.client.ts new file mode 100644 index 0000000..c894ff4 --- /dev/null +++ b/src/api/auth.client.ts @@ -0,0 +1,17 @@ +import { BaseClient } from "./base.client"; +import { LoginRequest, LoginResponse } from "./types/login"; +import { SignUpRequest } from "./types/signup"; + +export class AuthClient extends BaseClient { + async login(loginRequest: LoginRequest) { + return this.post("auth/login", loginRequest); + } + + async register(signUpRequest: SignUpRequest) { + return this.post("auth/register", signUpRequest); + } + + async logout() { + return this.post("auth/logout", {}); + } +} diff --git a/src/api/base.client.ts b/src/api/base.client.ts new file mode 100644 index 0000000..34c50cf --- /dev/null +++ b/src/api/base.client.ts @@ -0,0 +1,109 @@ +import { ClientResponse, ErrorResponse } from "@/types"; + +class ApiError extends Error { + statusCode: number; + error: string; + + constructor(statusCode: number, message: string, error: string) { + super(message); + this.statusCode = statusCode; + this.error = error; + this.name = "ApiError"; + } +} + +export class BaseClient { + base: string | undefined; + + constructor(base: string = process.env.NEXT_PUBLIC_SERVER_URL!) { + this.base = base; + } + + private async request( + endpoint: string, + method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE", + body?: any, + options: RequestInit = {} + ): Promise> { + if (!this.base) { + throw new Error("Base URL is not defined"); + } + + const fetchOptions: RequestInit = { + method, + headers: { + "Content-Type": "application/json", + ...options.headers, + }, + credentials: "include", + ...options, + }; + + if (method !== "GET" && method !== "DELETE") { + fetchOptions.body = body != null ? JSON.stringify(body) : undefined; + } + + try { + const response = await fetch(`${this.base}/${endpoint}`, fetchOptions); + + if (!response.ok) { + const errorData: ErrorResponse = await response.json(); + + throw new ApiError( + errorData.statusCode, + errorData.message, + errorData?.error + ); + } + + const contentType = response.headers.get("Content-Type"); + if (contentType && contentType.includes("application/json")) { + const data: T = await response.json(); + return { + data, + }; + } else { + return { + data: {} as T, + }; + } + } catch (error) { + console.error("An error occurred", error); + if (error instanceof ApiError) { + throw error; + } else { + throw new Error("An unexpected error occurred"); + } + } + } + + protected async get( + endpoint: string, + options: RequestInit = {} + ): Promise> { + return this.request(endpoint, "GET", options); + } + + protected async post( + endpoint: string, + body: any, + options: RequestInit = {} + ): Promise> { + return this.request(endpoint, "POST", body, options); + } + + protected async patch( + endpoint: string, + body: any, + options: RequestInit = {} + ): Promise> { + return this.request(endpoint, "PATCH", body, options); + } + + protected async delete( + endpoint: string, + options: RequestInit = {} + ): Promise> { + return this.request(endpoint, "DELETE", options); + } +} diff --git a/src/api/chat.client.ts b/src/api/chat.client.ts new file mode 100644 index 0000000..c9c5883 --- /dev/null +++ b/src/api/chat.client.ts @@ -0,0 +1,17 @@ +import { BaseClient } from "./base.client"; +import { ChatRequest, ChatRequestClose, ChatResponse } from "./types/chat"; + +export class ChatClient extends BaseClient { + async chats() { + return this.get("chats"); + } + async chatById(chatRequest: ChatRequest) { + return this.get(`chats/${chatRequest.id}`); + } + async closeChat(chatRequestClose: ChatRequestClose) { + return this.patch(`chats/${chatRequestClose.id}`, { + isOpen: false, + customerId: chatRequestClose.customerId, + }); + } +} diff --git a/src/api/guide.client.ts b/src/api/guide.client.ts new file mode 100644 index 0000000..cd265ec --- /dev/null +++ b/src/api/guide.client.ts @@ -0,0 +1,30 @@ +import { CreateReviewDto } from "@/app/(admin)/new_dashboard/guides/dto/CreateReviewDto"; +import { BaseClient } from "./base.client"; +import { CreateGuideRequest, Guide, UpdateGuideRequest } from "./types/Guide"; +import { Review } from "./types/Review"; + +export class GuideClient extends BaseClient { + async getAllGuides() { + return await this.get("guides"); + } + + async getGuide(guideId: number) { + return await this.get("guides/" + guideId); + } + + async createGuide(createGuideRequest: CreateGuideRequest) { + return await this.post("guides", createGuideRequest); + } + + async updateGuide(updateGuideRequest: UpdateGuideRequest) { + return await this.patch("guides/" + updateGuideRequest.id, { + title: updateGuideRequest.title, + contentHTML: updateGuideRequest.contentHTML, + categories: updateGuideRequest.categories, + }); + } + + async deleteGuide(guideId: number) { + return await this.delete("guides/" + guideId); + } +} diff --git a/src/api/issue.client.ts b/src/api/issue.client.ts new file mode 100644 index 0000000..80b33df --- /dev/null +++ b/src/api/issue.client.ts @@ -0,0 +1,12 @@ +import { BaseClient } from "./base.client"; +import { UpdateIssueRequest, Issue } from "./types/Issue"; + +export class IssueClient extends BaseClient { + async getIssue() { + return await this.get("issues"); + } + + async updateIssue(updateIssueRequest: UpdateIssueRequest) { + return await this.patch("issues", updateIssueRequest); + } +} diff --git a/src/api/model.client.ts b/src/api/model.client.ts new file mode 100644 index 0000000..e5a5164 --- /dev/null +++ b/src/api/model.client.ts @@ -0,0 +1,17 @@ +import { BaseClient } from "./base.client"; +import { + ModelCreateGuideRequest, + ModelCreateGuideResponse, +} from "./types/model"; + +export class ModelClient extends BaseClient { + constructor() { + super(process.env.NEXT_PUBLIC_MODEL_AI_URL!); + } + createGuide(modelCreateGuideRequest: ModelCreateGuideRequest) { + return this.post( + "openai/generate-guide", + modelCreateGuideRequest + ); + } +} diff --git a/src/api/review.client.ts b/src/api/review.client.ts new file mode 100644 index 0000000..7badffe --- /dev/null +++ b/src/api/review.client.ts @@ -0,0 +1,12 @@ +import { BaseClient } from "./base.client"; +import { CreateReviewRequest, Review } from "./types/Review"; + +export class ReviewClient extends BaseClient { + async getReviews(guideId: number) { + return await this.get("reviews/guide/" + guideId); + } + + async addReview(review: CreateReviewRequest) { + return await this.post("reviews", review); + } +} diff --git a/src/api/types/Guide.ts b/src/api/types/Guide.ts new file mode 100644 index 0000000..54bde28 --- /dev/null +++ b/src/api/types/Guide.ts @@ -0,0 +1,23 @@ +import { z } from "zod"; +import { userSchema } from "./User"; +import { reviewSchema } from "./Review"; + +const guideSchema = z.object({ + id: z.number(), + title: z.string(), + categories: z.array(z.string()).max(3), + contentHTML: z.string(), + creatorId: z.number(), + createdAt: z.date(), + creator: userSchema.optional(), + reviews: z.array(reviewSchema).optional(), +}); + +export type Guide = z.infer; + +export type CreateGuideRequest = Pick< + Guide, + "title" | "categories" | "contentHTML" +>; + +export type UpdateGuideRequest = Partial & { id: number }; diff --git a/src/api/types/Issue.ts b/src/api/types/Issue.ts new file mode 100644 index 0000000..2d60e45 --- /dev/null +++ b/src/api/types/Issue.ts @@ -0,0 +1,15 @@ +import { z } from "zod"; + +export const issueSchema = z.object({ + id: z.number(), + categories: z.array(z.string()), + singletonKey: z.number().default(1), +}); + +export const updateIssueDtoSchema = z.object({ + categories: z.array(z.string()), +}); + +export type UpdateIssueRequest = z.infer; + +export type Issue = z.infer; diff --git a/src/api/types/Review.ts b/src/api/types/Review.ts new file mode 100644 index 0000000..d302ddf --- /dev/null +++ b/src/api/types/Review.ts @@ -0,0 +1,36 @@ +import { z } from "zod"; +import { userSchema } from "./User"; + +export const reviewSchema = z.object({ + id: z.number(), + userId: z.number(), + guideId: z.number(), + rating: z.number().min(1).max(5), + title: z.string(), + comment: z.string().optional(), + createdAt: z.date(), + user: userSchema.optional(), +}); + +export type Review = z.infer; + +export const CreateReviewSchema = z.object({ + guideId: z.number({ + required_error: "Guide ID is required", + }), + + rating: z + .number({ + required_error: "Stars rating is required", + }) + .min(1, "Stars rating must be at least 1") + .max(5, "Stars rating cannot exceed 5"), + + comment: z.string().optional(), + + title: z.string({ + required_error: "Title is required", + }), +}); + +export type CreateReviewRequest = z.infer; diff --git a/src/api/types/User.ts b/src/api/types/User.ts new file mode 100644 index 0000000..27898ce --- /dev/null +++ b/src/api/types/User.ts @@ -0,0 +1,14 @@ +import { z } from "zod"; + +export const userSchema = z.object({ + id: z.number(), + username: z.string(), + email: z.string().email().optional(), + roles: z.array(z.string()), +}); + +export type User = z.infer; + +export type UserRole = Pick; + +export type UserInfo = Pick; diff --git a/src/api/types/chat.ts b/src/api/types/chat.ts new file mode 100644 index 0000000..685adb6 --- /dev/null +++ b/src/api/types/chat.ts @@ -0,0 +1,25 @@ +import { z } from "zod"; +import { messageSchema } from "./message"; +import { userSchema } from "./User"; + +export const chatSchema = z.object({ + id: z.number(), + customerId: z.number(), + isOpen: z.boolean(), + startTime: z.date(), + endTime: z.date().optional(), + messages: z.array(messageSchema), + user: userSchema, +}); + +export type Chat = z.infer; + +export type ChatRequest = { + id: number; +}; + +export type ChatRequestUpdate = Partial; + +export type ChatRequestClose = Pick; + +export type ChatResponse = Chat; diff --git a/src/api/types/login.ts b/src/api/types/login.ts new file mode 100644 index 0000000..c62219f --- /dev/null +++ b/src/api/types/login.ts @@ -0,0 +1,8 @@ +import { User } from "./User"; + +export type LoginRequest = { + username: string; + password: string; +}; + +export type LoginResponse = User; diff --git a/src/api/types/message.ts b/src/api/types/message.ts new file mode 100644 index 0000000..e62a5b0 --- /dev/null +++ b/src/api/types/message.ts @@ -0,0 +1,12 @@ +import { z } from "zod"; + +export const messageSchema = z.object({ + id: z.number(), + chatId: z.number(), + isSupportSender: z.boolean(), + content: z.string(), + isNote: z.boolean(), + timeStamp: z.date(), +}); + +export type Message = z.infer; diff --git a/src/api/types/model.ts b/src/api/types/model.ts new file mode 100644 index 0000000..e4a7a40 --- /dev/null +++ b/src/api/types/model.ts @@ -0,0 +1,12 @@ +import { Message } from "./message"; +import { User } from "./User"; + +export type ModelCreateGuideRequest = { + messages: Pick[]; + user: Pick; +}; + +export type ModelCreateGuideResponse = { + contentHTML: string; + title: string; +}; diff --git a/src/api/types/signup.ts b/src/api/types/signup.ts new file mode 100644 index 0000000..4344069 --- /dev/null +++ b/src/api/types/signup.ts @@ -0,0 +1,9 @@ +import { User } from "./User"; + +export type SignUpRequest = { + username: string; + email: string; + password: string; +}; + +export type SignUpResponse = User; diff --git a/src/app/(admin)/new_dashboard/components/ChatHeader/hooks/useChat.ts b/src/app/(admin)/new_dashboard/components/ChatHeader/hooks/useChat.ts new file mode 100644 index 0000000..a1d526a --- /dev/null +++ b/src/app/(admin)/new_dashboard/components/ChatHeader/hooks/useChat.ts @@ -0,0 +1,37 @@ +import { ChatClient } from "@/api/chat.client"; +import { ErrorResponse, isSuccessResponse } from "@/types"; +import { Contact } from "../../../types"; +import { useCloseChat } from "@/hooks"; +import { ChatRequestClose } from "@/api/types/chat"; + +interface Props { + handleContactSelect: (contact: Contact) => void; + selectedContact: Contact | null; +} + +export const useChat = ({ handleContactSelect, selectedContact }: Props) => { + const { mutate, error, isError } = useCloseChat(); + + async function handleCloseChat( + chatRequestClose: ChatRequestClose + ): Promise { + mutate(chatRequestClose, { + onSuccess: () => { + if (selectedContact) { + handleContactSelect({ + chatId: selectedContact.chatId, + isOpen: false, + userId: selectedContact.userId, + username: selectedContact.username, + }); + } + }, + }); + } + + return { + handleCloseChat, + chatError: error, + isChatError: isError, + }; +}; diff --git a/src/app/(admin)/new_dashboard/components/ChatHeader/hooks/useGuide.ts b/src/app/(admin)/new_dashboard/components/ChatHeader/hooks/useGuide.ts new file mode 100644 index 0000000..08b33aa --- /dev/null +++ b/src/app/(admin)/new_dashboard/components/ChatHeader/hooks/useGuide.ts @@ -0,0 +1,30 @@ +import { useGuideContext } from "@/app/providers/guide"; +import { SuccessResponse, ErrorResponse, isSuccessResponse } from "@/types"; +import { useRouter } from "next/navigation"; +import { ModelCreateGuideResponse } from "@/api/types/model"; +import { useModelGenerateGuide } from "@/hooks/api/modelHooks"; + +export const useGuide = () => { + const router = useRouter(); + const { setGuide } = useGuideContext(); + const { mutate, isError, error, isPending } = useModelGenerateGuide(); + + async function handleGenerateGuide(chatId: number): Promise { + mutate(chatId, { + onSuccess: (response) => { + const modelCreateGuideResponse = + response as SuccessResponse; + const guideResponse = modelCreateGuideResponse.data; + setGuide(guideResponse); + router.push("new_dashboard/guides/create"); + }, + }); + } + + return { + handleGenerateGuide, + isPending, + guideError: error, + isGuideError: isError, + }; +}; diff --git a/src/app/(admin)/new_dashboard/components/ChatHeader/index.tsx b/src/app/(admin)/new_dashboard/components/ChatHeader/index.tsx new file mode 100644 index 0000000..3d0de4c --- /dev/null +++ b/src/app/(admin)/new_dashboard/components/ChatHeader/index.tsx @@ -0,0 +1,103 @@ +import { + Box, + Typography, + Button, + Backdrop, + CircularProgress, +} from "@mui/material"; + +import SmartToyIcon from "@mui/icons-material/SmartToy"; +import DoneIcon from "@mui/icons-material/Done"; +import { Contact } from "../../types"; +import { useGuide } from "./hooks/useGuide"; +import { useState } from "react"; +import { useChat } from "./hooks/useChat"; + +type Props = { + selectedContact: Contact | null; + handleContactSelect: (contact: Contact) => void; +}; + +const ChatHeader = ({ selectedContact, handleContactSelect }: Props) => { + const [isLoading, setIsLoading] = useState(false); + const { handleGenerateGuide, guideError, isGuideError } = useGuide(); + const { handleCloseChat, chatError, isChatError } = useChat({ + handleContactSelect, + selectedContact, + }); + + return ( + <> + + {selectedContact && ( + <> + {selectedContact.username} + + + {isGuideError && ( + + {guideError?.message} + + )} + {isChatError && ( + + {chatError?.message} + + )} + + + + + + + + + )} + + + ); +}; + +export default ChatHeader; diff --git a/src/app/(admin)/new_dashboard/components/ChatPopup/index.tsx b/src/app/(admin)/new_dashboard/components/ChatPopup/index.tsx new file mode 100644 index 0000000..ef890dc --- /dev/null +++ b/src/app/(admin)/new_dashboard/components/ChatPopup/index.tsx @@ -0,0 +1,105 @@ +"use client"; + +import React, { useState } from "react"; +import { Box, IconButton, Paper, Typography, Slide } from "@mui/material"; +import { Chat, Minimize } from "@mui/icons-material"; +import SupportMessageList from "../SupportMessageList"; +import MessageInput from "@/common/components/MessageInput"; +import { Contact } from "../../types"; + +interface Props { + selectedContact: Contact | null; + isOpen: boolean; + setIsOpen: (isOpen: boolean) => void; +} + +const ChatPopup = ({ selectedContact, setIsOpen, isOpen }: Props) => { + const toggleChat = () => { + setIsOpen(!isOpen); + }; + + return ( + + {!isOpen && ( + + + + )} + + {isOpen && selectedContact && ( + + + + {selectedContact.username} + + + + + + + + + + {/* Input and Send Button */} + {selectedContact && selectedContact.isOpen && ( + + + + )} + + + )} + + ); +}; + +export default ChatPopup; diff --git a/src/app/(admin)/new_dashboard/components/ContactCard/index.tsx b/src/app/(admin)/new_dashboard/components/ContactCard/index.tsx new file mode 100644 index 0000000..c5b461d --- /dev/null +++ b/src/app/(admin)/new_dashboard/components/ContactCard/index.tsx @@ -0,0 +1,40 @@ +import React from "react"; +import { + Box, + ListItemButton, + ListItemAvatar, + Avatar, + ListItemText, +} from "@mui/material"; +import { Contact } from "../../types"; + +interface ContactCardProps { + contact: Contact; + selected: boolean; + onSelect: (contact: Contact) => void; +} + +const ContactCard: React.FC = ({ + contact, + selected, + onSelect, +}) => { + return ( + + onSelect(contact)} + sx={{ + borderRadius: 2, + }} + > + + + + + + + ); +}; + +export default ContactCard; diff --git a/src/app/(admin)/new_dashboard/components/ContactList/index.tsx b/src/app/(admin)/new_dashboard/components/ContactList/index.tsx new file mode 100644 index 0000000..7fe2887 --- /dev/null +++ b/src/app/(admin)/new_dashboard/components/ContactList/index.tsx @@ -0,0 +1,52 @@ +import { + Box, + List, + ListItemButton, + ListItemAvatar, + Avatar, + ListItemText, +} from "@mui/material"; +import { Contact } from "../../types"; +import ContactCard from "../ContactCard"; + +type Props = { + contacts: Contact[]; + selectedContact: Contact | null; + onSelectContact: (chat: Contact) => void; +}; + +const ContactList = ({ contacts, onSelectContact, selectedContact }: Props) => { + + const reversedContacts = [...contacts].reverse(); + + return ( + + + {reversedContacts.length === 0 ? ( + + No chats yet! + + ) : ( + + {reversedContacts.map((contact, index) => ( + + ))} + + )} + + + ); +}; + +export default ContactList; diff --git a/src/app/(admin)/new_dashboard/components/ContactSidebar/index.tsx b/src/app/(admin)/new_dashboard/components/ContactSidebar/index.tsx new file mode 100644 index 0000000..a46e42b --- /dev/null +++ b/src/app/(admin)/new_dashboard/components/ContactSidebar/index.tsx @@ -0,0 +1,81 @@ +"use client"; +import { + Box, + Drawer, + List, + ListItemButton, + ListItemAvatar, + Avatar, + ListItemText, + Typography, + Slide, + } from "@mui/material"; + import { useChat } from "@/app/hooks/useChat"; + import ContactCard from "../ContactCard"; + + interface Props { + isContactSidebarOpen: boolean; + } + + const ContactSidebar = ({ isContactSidebarOpen }: Props) => { + const sidebarWidth = "230px"; + const { selectedContact, contacts, handleContactSelect } = useChat(); + + const scrollbarStyles = { + "&::-webkit-scrollbar": { + width: "7px", + }, + "&::-webkit-scrollbar-thumb": { + backgroundColor: "primary.main", + borderRadius: "15px", + }, + }; + + return ( + + + + + Open chats + {contacts.length === 0 ? ( + + No chats yet! + + ) : ( + contacts.map((contact, index) => ( + + )) + )} + + + + + ); + }; + + export default ContactSidebar; + \ No newline at end of file diff --git a/src/app/(admin)/new_dashboard/components/SupportMessageList/index.tsx b/src/app/(admin)/new_dashboard/components/SupportMessageList/index.tsx new file mode 100644 index 0000000..c2506a3 --- /dev/null +++ b/src/app/(admin)/new_dashboard/components/SupportMessageList/index.tsx @@ -0,0 +1,56 @@ +import { Box, List } from "@mui/material"; +import { useSocket } from "@/app/hooks/useSocket"; +import { useMessageList } from "@/common/hooks/useMessageList"; +import { MessageContainer } from "@/common"; +type Props = { + chatId: number; + username: string; +}; + +const SupportMessageList = ({ chatId, username }: Props) => { + const socket = useSocket(); + const { messages } = useMessageList({ chatId, socket }); + + return ( + theme.palette.background.paper, + }} + > + + + {Object.keys(messages).map((date, index) => { + return ( + + + {date} + + {messages[date].map((msg, index) => { + return ( + + ); + })} + + ); + })} + + + + ); +}; + +export default SupportMessageList; diff --git a/src/app/(admin)/new_dashboard/guides/[id]/page.tsx b/src/app/(admin)/new_dashboard/guides/[id]/page.tsx new file mode 100644 index 0000000..91277fd --- /dev/null +++ b/src/app/(admin)/new_dashboard/guides/[id]/page.tsx @@ -0,0 +1,74 @@ +"use client"; + +import React from "react"; +import PageContainer from "@/components/PageContainer"; +import { useParams } from "next/navigation"; +import DashboardCard from "../../shared/Card"; + +import { useGuide } from "@/hooks"; + +import { + Typography, + CircularProgress, + Alert, + Box, + Divider, +} from "@mui/material"; +import ReviewList from "../components/ReviewList"; +import parse from "html-react-parser"; +import "quill/dist/quill.snow.css"; +import { useChat } from "@/app/hooks/useChat"; +import DeleteEditButtons from "../components/Buttons"; + +const GuidePage: React.FC = () => { + const params = useParams(); + + const id = params?.id ? Number(params.id) : null; + + const { + data: response, + isLoading, + isError, + error, + isSuccess, + } = useGuide(id ?? 0); + + if (isLoading) { + return ; + } + + if (isError) { + return {error.message}; + } + + if (isSuccess && "data" in response) { + const guide = response.data; + const creatorAndDateInfo = `Created by ${ + guide.creator?.username + } on ${new Date(guide.createdAt).toLocaleDateString()}`; + + if (!guide.title) { + return Guide not found; + } + + return ( + + } + title={guide.title} + subtitle={creatorAndDateInfo} + > + + {parse(guide.contentHTML)} + + + + + ); + } +}; + +export default GuidePage; diff --git a/src/app/(admin)/new_dashboard/guides/components/AddReviewBox/index.tsx b/src/app/(admin)/new_dashboard/guides/components/AddReviewBox/index.tsx new file mode 100644 index 0000000..7347d40 --- /dev/null +++ b/src/app/(admin)/new_dashboard/guides/components/AddReviewBox/index.tsx @@ -0,0 +1,94 @@ +"use client"; +import React from 'react'; +import { Box, Typography, Rating, TextField, Button, CircularProgress, Alert } from '@mui/material'; +import useAddReviewForm from '../../hooks/useAddReviewForm'; +import DashboardCard from '../../../shared/Card'; + +interface AddReviewBoxProps { + guideId: number; +} + +const AddReviewBox: React.FC = ({ guideId }) => { + + const { + comment, + setComment, + title, + setTitle, + stars, + setStars, + validationError, + isError, + error, + isPending, + isSuccess, + handleSubmit, + } = useAddReviewForm(guideId); + + if (!guideId) { + return Guide not found; + } + + return ( + + + + { + setStars(newValue ?? 1); + }} + /> + + setTitle(e.target.value)} + sx={{ mb: 2 }} + /> + setComment(e.target.value)} + /> + + + + {validationError && ( + {validationError} + )} + {isError && ( + + {error?.message} + + )} + {isSuccess && ( + + Review submitted successfully! + + )} + + + ); +}; + +export default AddReviewBox; diff --git a/src/app/(admin)/new_dashboard/guides/components/Buttons/index.tsx b/src/app/(admin)/new_dashboard/guides/components/Buttons/index.tsx new file mode 100644 index 0000000..dc7cf80 --- /dev/null +++ b/src/app/(admin)/new_dashboard/guides/components/Buttons/index.tsx @@ -0,0 +1,38 @@ +"use client"; +import React from "react"; +import { Box, IconButton } from "@mui/material"; +import ModeEditOutlineOutlinedIcon from "@mui/icons-material/ModeEditOutlineOutlined"; +import DeleteOutlinedIcon from "@mui/icons-material/DeleteOutlined"; +import { useParams, useRouter } from "next/navigation"; +import { useDeleteGuide } from "../../hooks/useDeleteGuide"; + +const DeleteEditButtons = () => { + const router = useRouter(); + const params = useParams(); + + const id = params?.id ? Number(params.id) : 0; + + const { handleDelete } = useDeleteGuide(id); + + const handleEdit = () => { + router.push(`/new_dashboard/guides/edit/${id}`); + }; + + return ( + + + + + + + + + ); +}; + +export default DeleteEditButtons; diff --git a/src/app/(admin)/new_dashboard/guides/components/Editor/index.tsx b/src/app/(admin)/new_dashboard/guides/components/Editor/index.tsx new file mode 100644 index 0000000..85d979a --- /dev/null +++ b/src/app/(admin)/new_dashboard/guides/components/Editor/index.tsx @@ -0,0 +1,160 @@ +import dynamic from "next/dynamic"; +import { BaseSyntheticEvent, useEffect, useState } from "react"; +import "react-quill/dist/quill.snow.css"; +import { Box, Button, TextField, Autocomplete, Chip } from "@mui/material"; +import SaveAsIcon from "@mui/icons-material/SaveAs"; +import { + UseFormRegister, + UseFormSetValue, + UseFormWatch, +} from "react-hook-form"; +import { CreateGuideRequest } from "@/api/types/Guide"; +import { LoadingButton } from "@mui/lab"; + +const DynamicReactQuill = dynamic(() => import("react-quill")); + +interface Props { + initialTitle?: string; + initialContent?: string; + categories?: string[]; + allCategories?: string[]; + onSave: (e?: BaseSyntheticEvent) => void; + register: UseFormRegister; + error: Error | null; + setValue: UseFormSetValue; + watch: UseFormWatch; +} + +const GuideEditor = ({ + initialContent = "", + initialTitle = "", + categories, + onSave, + allCategories = ["Internet", "Router", "Wi-Fi", "Network"], + register, + error, + setValue, + watch, +}: Props) => { + const modules = { + toolbar: [ + [{ header: [1, 2, 3, 4, 5, 6, false] }], + [{ font: [] }], + ["bold", "italic", "underline", "strike", "blockquote"], + [{ align: ["right", "center", "justify"] }], + [{ list: "ordered" }, { list: "bullet" }], + ["link", "image"], + [{ color: [] }], + [{ background: [] }], + ], + }; + + const formats = [ + "header", + "font", + "bold", + "italic", + "underline", + "strike", + "blockquote", + "list", + "bullet", + "link", + "color", + "image", + "background", + "align", + ]; + + return ( + + setValue("title", e.target.value)} + sx={{ mb: 2 }} + /> + + { + setValue("categories", newValue); + }} + renderTags={(value: string[], getTagProps) => + value.map((option: string, index: number) => ( + + + + )) + } + renderInput={(params) => ( + + )} + sx={{ mt: 2, mb: 2 }} + /> + + setValue("contentHTML", content)} + /> + + + + } + type="submit" + sx={{ + width: "15%", + }} + > + Save Guide + + + + ); +}; + +export default GuideEditor; diff --git a/src/app/(admin)/new_dashboard/guides/components/GuideCard/index.tsx b/src/app/(admin)/new_dashboard/guides/components/GuideCard/index.tsx new file mode 100644 index 0000000..22f758e --- /dev/null +++ b/src/app/(admin)/new_dashboard/guides/components/GuideCard/index.tsx @@ -0,0 +1,69 @@ +import { + Card, + CardContent, + Typography, + Box, + Rating, + Chip, +} from "@mui/material"; +import SupportIcon from "@mui/icons-material/Support"; +import { useTheme } from "@mui/material/styles"; +import { Guide } from "@/api/types/Guide"; + +type Props = { + guide: Guide; +}; + +const GuideCard = ({ guide }: Props) => { + const theme = useTheme(); + const totalStars = + guide.reviews?.reduce((acc, review) => acc + review.rating, 0) ?? 0; + const avgRating = totalStars / (guide.reviews?.length ?? 1); + + return ( + + + + + + {guide.title} + + + Created by {guide.creator?.username} at{" "} + {new Date(guide.createdAt).toLocaleDateString()} + + + + + {guide.categories.map((category) => ( + + ))} + + + + + + ({guide.reviews?.length ?? 0}) + + + + + ); +}; + +export default GuideCard; diff --git a/src/app/(admin)/new_dashboard/guides/components/GuideList/index.tsx b/src/app/(admin)/new_dashboard/guides/components/GuideList/index.tsx new file mode 100644 index 0000000..7a47319 --- /dev/null +++ b/src/app/(admin)/new_dashboard/guides/components/GuideList/index.tsx @@ -0,0 +1,42 @@ +import { Grid, Typography } from "@mui/material"; +import GuideCard from "../GuideCard"; +import { Guide } from "@/api/types/Guide"; +import { useRouter } from "next/navigation"; + +type Props = { + guideItems: Guide[]; +}; + +const GuideList = ({ guideItems }: Props) => { + const router = useRouter(); + + const handleCardClick = (guideId: number) => { + router.push(`guides/${guideId}`); + }; + + return ( + + {guideItems.length > 0 ? ( + guideItems.map((guide) => ( + handleCardClick(guide.id)} + style={{ cursor: "pointer" }} + > + + + )) + ) : ( + + No guides available + + )} + + ); +}; + +export default GuideList; diff --git a/src/app/(admin)/new_dashboard/guides/components/ReviewCard/index.tsx b/src/app/(admin)/new_dashboard/guides/components/ReviewCard/index.tsx new file mode 100644 index 0000000..fd0cd95 --- /dev/null +++ b/src/app/(admin)/new_dashboard/guides/components/ReviewCard/index.tsx @@ -0,0 +1,49 @@ +import { Review } from "@/api/types/Review"; +import { Typography, Box, Rating, Divider } from "@mui/material"; +import React from "react"; + +interface ReviewCardProps { + review: Review; +} + +const ReviewCard: React.FC = ({ review }) => { + return ( + + + + + {review.user?.username} + + + {new Date(review.createdAt).toLocaleDateString()} + + + + + + {review.title} + + + + {review.comment} + + + + ); +}; + +export default ReviewCard; diff --git a/src/app/(admin)/new_dashboard/guides/components/ReviewList/index.tsx b/src/app/(admin)/new_dashboard/guides/components/ReviewList/index.tsx new file mode 100644 index 0000000..83a3038 --- /dev/null +++ b/src/app/(admin)/new_dashboard/guides/components/ReviewList/index.tsx @@ -0,0 +1,59 @@ +"use client"; + +import React, { useState } from "react"; +import { Typography, Box, Button, Divider } from "@mui/material"; +import { Review } from "@/api/types/Review"; +import AddReviewBox from "../AddReviewBox"; +import ReviewCard from "../ReviewCard"; +import DashboardCard from "../../../shared/Card"; + +interface ReviewListProps { + reviews: Review[]; + guideId: number; +} + +const ReviewList: React.FC = ({ reviews, guideId }) => { + const [visibleCount, setVisibleCount] = useState(1); + + const showMoreReviews = () => { + setVisibleCount((prevCount) => Math.min(prevCount + 2, reviews.length)); + }; + + const showLessReviews = () => { + setVisibleCount(1); + }; + + let reviewsContext = !reviews.length ? ( + No reviews available yet + ) : ( + reviews + .slice(0, visibleCount) + .map((review, index) => ) + ); + + return ( + + + + {reviewsContext} + {reviews?.length > 1 && ( + + + + )} + + + + + ); +}; + +export default ReviewList; diff --git a/src/app/(admin)/new_dashboard/guides/components/SearchBar/index.tsx b/src/app/(admin)/new_dashboard/guides/components/SearchBar/index.tsx new file mode 100644 index 0000000..164efa6 --- /dev/null +++ b/src/app/(admin)/new_dashboard/guides/components/SearchBar/index.tsx @@ -0,0 +1,81 @@ +import { + TextField, + InputAdornment, + Box, + FormControl, + Select, + MenuItem, + SelectChangeEvent, + InputLabel, +} from "@mui/material"; +import SearchIcon from "@mui/icons-material/Search"; +import Autocomplete from "@mui/material/Autocomplete"; +import { SortCriteria, sortOptions } from "../../types"; + +type Props = { + searchQuery: string; + onSearchChange: (e: React.ChangeEvent) => void; + selectedTag: string; + onTagChange: (e: React.ChangeEvent<{}>, value: string | null) => void; + categories: string[]; + sortCriteria: SortCriteria; + handleSortChange: ( + e: React.ChangeEvent + ) => void; +}; + +const SearchBar = ({ + searchQuery, + onSearchChange, + selectedTag, + onTagChange, + categories, + sortCriteria, + handleSortChange, +}: Props) => ( + + + + + ), + }} + /> + + ( + + )} + isOptionEqualToValue={(option, value) => option === value} + getOptionLabel={(option) => option} + /> + + + handleSortChange(e)} + variant="outlined" + > + {sortOptions.map((option) => ( + + {option} + + ))} + + + +); + +export default SearchBar; diff --git a/src/app/(admin)/new_dashboard/guides/create/hooks/useCreateGuide.ts b/src/app/(admin)/new_dashboard/guides/create/hooks/useCreateGuide.ts new file mode 100644 index 0000000..33ff433 --- /dev/null +++ b/src/app/(admin)/new_dashboard/guides/create/hooks/useCreateGuide.ts @@ -0,0 +1,56 @@ +import { CreateGuideRequest } from "@/api/types/Guide"; +import { useGuideContext } from "@/app/providers/guide"; +import { useCreateGuide } from "@/hooks"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { schema } from "../validations/schema"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { useCategories } from "../../hooks/useCategories"; + +export const useGuide = () => { + const { + register, + handleSubmit, + setValue, + watch, + formState: { errors, isValid }, + } = useForm({ + resolver: zodResolver(schema), + mode: "onChange", + reValidateMode: "onChange", + defaultValues: { + categories: [], + }, + }); + const { guide } = useGuideContext(); + + const router = useRouter(); + + const { mutate, isError, error, isPending } = useCreateGuide(); + + const onSubmit = (data: CreateGuideRequest) => { + mutate(data, { + onSuccess: (response) => { + console.log(response); + router.push("/new_dashboard/guides"); + }, + }); + }; + + const { categories } = useCategories(); + + return { + guide, + categories, + register, + setValue, + watch, + handleSubmit: handleSubmit(onSubmit), + errors, + isValid, + isError, + error, + isPending, + }; +}; diff --git a/src/app/(admin)/new_dashboard/guides/create/page.tsx b/src/app/(admin)/new_dashboard/guides/create/page.tsx new file mode 100644 index 0000000..705d85d --- /dev/null +++ b/src/app/(admin)/new_dashboard/guides/create/page.tsx @@ -0,0 +1,36 @@ +"use client"; +import PageContainer from "@/components/PageContainer"; +import DashboardCard from "../../shared/Card"; +import { Box } from "@mui/material"; +import GuideEditor from "../components/Editor"; +import { useGuide } from "./hooks/useCreateGuide"; +import { useCategories } from "../hooks/useCategories"; + +const Page = () => { + const { guide, categories, register, error, handleSubmit, setValue, watch } = + useGuide(); + return ( + + + + + + + + ); +}; + +export default Page; diff --git a/src/app/(admin)/new_dashboard/guides/create/validations/schema.ts b/src/app/(admin)/new_dashboard/guides/create/validations/schema.ts new file mode 100644 index 0000000..04cfef3 --- /dev/null +++ b/src/app/(admin)/new_dashboard/guides/create/validations/schema.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const schema = z.object({ + contentHTML: z.string().min(1, "Content is required"), + title: z.string().min(1, "Title is required"), + categories: z.array(z.string()).max(3, "You can select up to 3 categories"), +}); diff --git a/src/app/(admin)/new_dashboard/guides/dto/CreateReviewDto.ts b/src/app/(admin)/new_dashboard/guides/dto/CreateReviewDto.ts new file mode 100644 index 0000000..dffd468 --- /dev/null +++ b/src/app/(admin)/new_dashboard/guides/dto/CreateReviewDto.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; + +export const CreateReviewSchema = z.object({ + guideId: z.number({ + required_error: "Guide ID is required", + }), + rating: z + .number({ + required_error: "Stars rating is required", + }) + .min(1, "Stars rating must be at least 1") + .max(5, "Stars rating cannot exceed 5"), + + title: z.string(), + + comment: z.string().optional(), +}); + +export type CreateReviewDto = z.infer; diff --git a/src/app/(admin)/new_dashboard/guides/edit/[id]/hooks/useGetGuide.ts b/src/app/(admin)/new_dashboard/guides/edit/[id]/hooks/useGetGuide.ts new file mode 100644 index 0000000..6ffb655 --- /dev/null +++ b/src/app/(admin)/new_dashboard/guides/edit/[id]/hooks/useGetGuide.ts @@ -0,0 +1,25 @@ +import { Guide } from "@/api/types/Guide"; +import { useOnFetch } from "@/common/hooks/useOnFetch"; +import { useGuide } from "@/hooks"; +import { ClientResponse, SuccessResponse } from "@/types"; +import { useState } from "react"; + +export const useGetGuide = (id: number) => { + const { data, isError, error, isLoading } = useGuide(id); + const [guide, setGuide] = useState(); + + useOnFetch( + (clientResponse: ClientResponse) => { + if (isError) { + throw error; + } else { + const successResponse = clientResponse as SuccessResponse; + setGuide(successResponse.data); + } + }, + !!data || isError, + data + ); + + return { guide, isLoading }; +}; diff --git a/src/app/(admin)/new_dashboard/guides/edit/[id]/hooks/useUpdateGuide.ts b/src/app/(admin)/new_dashboard/guides/edit/[id]/hooks/useUpdateGuide.ts new file mode 100644 index 0000000..2c9611b --- /dev/null +++ b/src/app/(admin)/new_dashboard/guides/edit/[id]/hooks/useUpdateGuide.ts @@ -0,0 +1,46 @@ +import { CreateGuideRequest, UpdateGuideRequest } from "@/api/types/Guide"; +import { useUpdateGuide } from "@/hooks"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { schema } from "../../../validations/guideSchema"; +import { useRouter } from "next/navigation"; + +export const useUpdateGuideHook = (id: number) => { + const { + register, + handleSubmit, + setValue, + watch, + formState: { errors }, + } = useForm({ + resolver: zodResolver(schema), + mode: "onChange", + reValidateMode: "onChange", + }); + const router = useRouter(); + const { mutate, isError, error } = useUpdateGuide(); + + const handleSave = (createGuideRequest: CreateGuideRequest) => { + mutate( + { ...createGuideRequest, id }, + { + onSuccess: () => { + router.push("/new_dashboard/guides/" + id); + }, + onError: (error) => { + console.log(error); + }, + } + ); + }; + + return { + register, + handleSubmit: handleSubmit(handleSave), + setValue, + errors, + isServerError: isError, + serverError: error, + watch, + }; +}; diff --git a/src/app/(admin)/new_dashboard/guides/edit/[id]/page.tsx b/src/app/(admin)/new_dashboard/guides/edit/[id]/page.tsx new file mode 100644 index 0000000..6dd4176 --- /dev/null +++ b/src/app/(admin)/new_dashboard/guides/edit/[id]/page.tsx @@ -0,0 +1,58 @@ +"use client"; +import PageContainer from "@/components/PageContainer"; +import DashboardCard from "../../../shared/Card"; +import { Box } from "@mui/material"; +import GuideEditor from "../../components/Editor"; +import { useUpdateGuideHook } from "./hooks/useUpdateGuide"; +import { useParams } from "next/navigation"; +import { useGetGuide } from "./hooks/useGetGuide"; +import { useEffect } from "react"; + +const Page = () => { + const params = useParams(); + const id = params?.id ? Number(params.id) : 0; + const { register, handleSubmit, serverError, setValue, watch } = + useUpdateGuideHook(id); + + const { guide, isLoading } = useGetGuide(id); + useEffect(() => { + if (guide) { + setValue("title", guide?.title); + setValue("categories", guide?.categories); + setValue("contentHTML", guide?.contentHTML); + } + }, [guide]); + if (isLoading) { + return "Loading..."; + } + + const handleSave = (e) => { + e.preventDefault(); + handleSubmit(); + }; + + return ( + + + + + + + + ); +}; + +export default Page; diff --git a/src/app/(admin)/new_dashboard/guides/hooks/index.ts b/src/app/(admin)/new_dashboard/guides/hooks/index.ts new file mode 100644 index 0000000..22228ae --- /dev/null +++ b/src/app/(admin)/new_dashboard/guides/hooks/index.ts @@ -0,0 +1,2 @@ +export * from "./useSearchGuides"; +export * from "./useAddReviewForm"; diff --git a/src/app/(admin)/new_dashboard/guides/hooks/sortGuides.ts b/src/app/(admin)/new_dashboard/guides/hooks/sortGuides.ts new file mode 100644 index 0000000..c26ebbf --- /dev/null +++ b/src/app/(admin)/new_dashboard/guides/hooks/sortGuides.ts @@ -0,0 +1,15 @@ +import { Guide } from "@/api/types/Guide"; +import { calculateAvgRating } from "@/util/calculateAvgRating"; + +export const sortByCriteria = (guides: Guide[], sortCriteria: string): Guide[] => { + return guides.sort((guide1, guide2) => { + if (sortCriteria === "rating") { + const avgRatingA = calculateAvgRating(guide1); + const avgRatingB = calculateAvgRating(guide2); + return avgRatingB - avgRatingA; + } else if (sortCriteria === "date") { + return new Date(guide2.createdAt).getTime() - new Date(guide1.createdAt).getTime(); + } + return 0; + }); +}; \ No newline at end of file diff --git a/src/app/(admin)/new_dashboard/guides/hooks/useAddReviewForm.ts b/src/app/(admin)/new_dashboard/guides/hooks/useAddReviewForm.ts new file mode 100644 index 0000000..2971ea5 --- /dev/null +++ b/src/app/(admin)/new_dashboard/guides/hooks/useAddReviewForm.ts @@ -0,0 +1,53 @@ +import { useState } from "react"; +import { CreateReviewDto, CreateReviewSchema } from "../dto/CreateReviewDto"; +import { useAddReview } from "@/hooks"; + +const useAddReviewForm = (guideId: number) => { + const [comment, setComment] = useState(""); + const [title, setTitle] = useState(""); + const [stars, setStars] = useState(0); + const [validationError, setValidationError] = useState(null); + const { mutate, isError, error, isPending, isSuccess } = useAddReview(); + + const handleSubmit = () => { + const newReview: CreateReviewDto = { + guideId, + rating: stars ?? 0, + title: title.trim(), + comment: comment.trim() || "", + }; + const validation = CreateReviewSchema.safeParse(newReview); + if (!validation.success) { + setValidationError( + validation.error.errors.map((err) => err.message).join(", ") + ); + return; + } + + mutate(validation.data, { + onSuccess: () => { + setComment(""); + setTitle(""); + setStars(0); + }, + }); + }; + + return { + comment, + setComment, + title, + setTitle, + stars, + setStars, + validationError, + setValidationError, + isError, + error, + isPending, + isSuccess, + handleSubmit, + }; +}; + +export default useAddReviewForm; diff --git a/src/app/(admin)/new_dashboard/guides/hooks/useCategories.ts b/src/app/(admin)/new_dashboard/guides/hooks/useCategories.ts new file mode 100644 index 0000000..4c34fea --- /dev/null +++ b/src/app/(admin)/new_dashboard/guides/hooks/useCategories.ts @@ -0,0 +1,10 @@ +import { useIssue } from "@/hooks/api/issueHooks"; + +export const useCategories = () => { + const { data: issue, error, isLoading, isSuccess } = useIssue(); + let categories: string[] = []; + if (isSuccess && "data" in issue) { + categories = issue.data.categories.toSorted(); + } + return { categories, error, isLoading }; +}; diff --git a/src/app/(admin)/new_dashboard/guides/hooks/useDeleteGuide.ts b/src/app/(admin)/new_dashboard/guides/hooks/useDeleteGuide.ts new file mode 100644 index 0000000..4bb07e9 --- /dev/null +++ b/src/app/(admin)/new_dashboard/guides/hooks/useDeleteGuide.ts @@ -0,0 +1,16 @@ +import { useDeleteGuide as useDeleteGuideHook } from "@/hooks"; +import { useRouter } from "next/navigation"; + +export const useDeleteGuide = (id: number) => { + const router = useRouter(); + const { mutate, error, isError } = useDeleteGuideHook(); + + const handleDelete = () => { + mutate(id, { + onSuccess: () => { + router.push("/new_dashboard/guides"); + }, + }); + }; + return { handleDelete, error, isError }; +}; diff --git a/src/app/(admin)/new_dashboard/guides/hooks/useGuideItems.ts b/src/app/(admin)/new_dashboard/guides/hooks/useGuideItems.ts new file mode 100644 index 0000000..47b545f --- /dev/null +++ b/src/app/(admin)/new_dashboard/guides/hooks/useGuideItems.ts @@ -0,0 +1,21 @@ +import { useAllGuides } from "@/hooks"; +import { Guide } from "@/api/types/Guide"; + +export const useGuideItems = () => { + const { + data: guideItems, + isLoading: isLoadingGuides, + error: guidesError, + isError: isGuidesError, + isSuccess: isGuidesSuccess, + } = useAllGuides(); + + let guides: Guide[] = []; + + if (isGuidesSuccess && "data" in guideItems) { + guides = guideItems.data; + } + + return { guides, isLoadingGuides, guidesError, isGuidesError, isGuidesSuccess }; + +}; diff --git a/src/app/(admin)/new_dashboard/guides/hooks/useIsCreateGuidePage.ts b/src/app/(admin)/new_dashboard/guides/hooks/useIsCreateGuidePage.ts new file mode 100644 index 0000000..87bb5cb --- /dev/null +++ b/src/app/(admin)/new_dashboard/guides/hooks/useIsCreateGuidePage.ts @@ -0,0 +1,13 @@ +import { usePathname } from "next/navigation"; +import { useEffect, useState } from "react"; + +export const useIsCreateGuidePage = () => { + const pathname = usePathname(); + + const [IsCreateGuidePage, setIsCreateGuidePage] = useState(false); + + useEffect(() => { + setIsCreateGuidePage(pathname === "/new_dashboard/guides/create"); + }, [IsCreateGuidePage, pathname]); + return IsCreateGuidePage; +}; diff --git a/src/app/(admin)/new_dashboard/guides/hooks/useIsDashboardPage.ts b/src/app/(admin)/new_dashboard/guides/hooks/useIsDashboardPage.ts new file mode 100644 index 0000000..9cab2b5 --- /dev/null +++ b/src/app/(admin)/new_dashboard/guides/hooks/useIsDashboardPage.ts @@ -0,0 +1,14 @@ +import { usePathname } from "next/navigation"; +import { useEffect, useState } from "react"; + +export const useIsDashboardPage = () => { + const pathname = usePathname(); + + const [isDashboardPage, setIsDashboardPage] = useState(false); + + useEffect(() => { + setIsDashboardPage(pathname === "/new_dashboard"); + console.log(isDashboardPage, "isDashboardPage"); + }, [isDashboardPage,pathname]); + return isDashboardPage; +}; diff --git a/src/app/(admin)/new_dashboard/guides/hooks/useSearchGuides.ts b/src/app/(admin)/new_dashboard/guides/hooks/useSearchGuides.ts new file mode 100644 index 0000000..dfaa6a0 --- /dev/null +++ b/src/app/(admin)/new_dashboard/guides/hooks/useSearchGuides.ts @@ -0,0 +1,44 @@ +import { useState } from "react"; +import { Guide } from "@/api/types/Guide"; +import { SelectChangeEvent } from "@mui/material"; +import { SortCriteria } from "../types"; +import { sortByCriteria } from "./sortGuides"; + +export const useSearchGuides = (initialGuides: Guide[]) => { + const [searchQuery, setSearchQuery] = useState(""); + const [selectedTag, setSelectedTag] = useState("All"); + const [sortCriteria, setSortCriteria] = useState("rating"); + + const filteredGuides = initialGuides.filter((guide) => { + const matchesQuery = guide.title + .toLowerCase() + .includes(searchQuery.toLowerCase()); + const matchesTag = + selectedTag === "All" || guide.categories.includes(selectedTag); + return matchesQuery && matchesTag; + }); + + const sortedAndFilteredGuides = sortByCriteria(filteredGuides, sortCriteria); + + const handleSearchChange = (event: React.ChangeEvent) => { + setSearchQuery(event.target.value); + }; + + const handleTagChange = (_: React.ChangeEvent<{}>, value: string | null) => { + setSelectedTag(value || "All"); + }; + + const handleSortChange = (event: React.ChangeEvent ) => { + setSortCriteria(event.target.value as SortCriteria); + }; + + return { + searchQuery, + selectedTag, + sortCriteria, + filteredGuides: sortedAndFilteredGuides, + handleSearchChange, + handleTagChange, + handleSortChange, + }; +}; diff --git a/src/app/(admin)/new_dashboard/guides/layout.tsx b/src/app/(admin)/new_dashboard/guides/layout.tsx new file mode 100644 index 0000000..9211007 --- /dev/null +++ b/src/app/(admin)/new_dashboard/guides/layout.tsx @@ -0,0 +1,16 @@ +"use client" +import { ReactNode } from "react"; +import ChatPopup from "../components/ChatPopup"; +import { useChat } from "@/app/hooks/useChat"; + +const GuideLayout = ({ children }: { children: ReactNode }) => { + const { selectedContact } = useChat(); + + return ( + <> + {children} + + ); +}; + +export default GuideLayout; diff --git a/src/app/(admin)/new_dashboard/guides/page.tsx b/src/app/(admin)/new_dashboard/guides/page.tsx new file mode 100644 index 0000000..e063927 --- /dev/null +++ b/src/app/(admin)/new_dashboard/guides/page.tsx @@ -0,0 +1,73 @@ +"use client"; +import DashboardCard from "../shared/Card"; +import PageContainer from "@/components/PageContainer"; +import { Alert, Box, CircularProgress, Typography } from "@mui/material"; +import GuideList from "./components/GuideList"; +import SearchBar from "./components/SearchBar"; +import { useSearchGuides } from "./hooks"; +import { useGuideItems } from "./hooks/useGuideItems"; +import { useCategories } from "./hooks/useCategories"; + +const GuidesListPage = () => { + const { + guides, + isLoadingGuides, + guidesError, + isGuidesError, + isGuidesSuccess, + } = useGuideItems(); + const { + categories, + error: issuesError, + isLoading: isLoadingIssues, + } = useCategories(); + const { + searchQuery, + selectedTag, + filteredGuides, + sortCriteria, + handleSearchChange, + handleTagChange, + handleSortChange + } = + useSearchGuides(guides); + + + return ( + + + + {(isLoadingGuides || isLoadingIssues) && } + {(isGuidesError || issuesError) && ( + + {guidesError?.message || + issuesError?.message || + "An error occurred"} + + )} + {isGuidesSuccess && guides.length > 0 && ( + <> + + + + + + )} + {isGuidesSuccess && guides.length === 0 && ( + No guides available + )} + + + + ); +}; + +export default GuidesListPage; diff --git a/src/app/(admin)/new_dashboard/guides/types/index.ts b/src/app/(admin)/new_dashboard/guides/types/index.ts new file mode 100644 index 0000000..8037824 --- /dev/null +++ b/src/app/(admin)/new_dashboard/guides/types/index.ts @@ -0,0 +1,10 @@ +export type Guide = { + id: number; + title: string; + issue: string; + likes: number; + dislikes: number; +}; + +export type SortCriteria = "rating" | "date"; +export const sortOptions = ["rating", "date"]; \ No newline at end of file diff --git a/src/app/(admin)/new_dashboard/guides/validations/guideSchema.ts b/src/app/(admin)/new_dashboard/guides/validations/guideSchema.ts new file mode 100644 index 0000000..04cfef3 --- /dev/null +++ b/src/app/(admin)/new_dashboard/guides/validations/guideSchema.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const schema = z.object({ + contentHTML: z.string().min(1, "Content is required"), + title: z.string().min(1, "Title is required"), + categories: z.array(z.string()).max(3, "You can select up to 3 categories"), +}); diff --git a/src/app/(admin)/new_dashboard/hooks/index.ts b/src/app/(admin)/new_dashboard/hooks/index.ts new file mode 100644 index 0000000..34eddca --- /dev/null +++ b/src/app/(admin)/new_dashboard/hooks/index.ts @@ -0,0 +1 @@ +export * from "./useContacts"; diff --git a/src/app/(admin)/new_dashboard/hooks/useContacts.ts b/src/app/(admin)/new_dashboard/hooks/useContacts.ts new file mode 100644 index 0000000..94a0dc1 --- /dev/null +++ b/src/app/(admin)/new_dashboard/hooks/useContacts.ts @@ -0,0 +1,83 @@ +"use client"; +import { useEffect, useState } from "react"; +import { ClientResponse, SuccessResponse } from "@/types"; +import { useChats } from "@/hooks/api/chatHooks"; +import { useOnFetch } from "@/common/hooks/useOnFetch"; +import { Socket } from "socket.io-client"; +import { Contact } from "../types"; +import { Chat } from "@/api/types/chat"; + +type Props = { + socket: Socket; +}; + +export const useContacts = ({ socket }: Props) => { + const [contacts, setContacts] = useState([]); + const { isError, data, error } = useChats(); + const [selectedContact, setSelectedContact] = useState(null); + + useEffect(() => { + if (socket && selectedContact && !selectedContact.isOpen) { + setContacts( + contacts.map((contact) => + contact.chatId === selectedContact.chatId + ? { ...contact, isOpen: false } + : contact + ) + ); + return; + } + if (socket && selectedContact) { + socket.emit("join", { chatId: selectedContact.chatId }); + return () => { + socket.emit("leave", { chatId: selectedContact.chatId }); + }; + } + }, [selectedContact]); + + const handleContactSelect = (contact: Contact) => { + console.log(contact); + setSelectedContact(contact); + }; + + useOnFetch( + (clientResponse: ClientResponse) => { + if (isError) { + throw error; + } else { + const successResponse = clientResponse as SuccessResponse; + const chats = successResponse.data; + const contacts = chats.map((chat) => ({ + chatId: chat.id, + userId: chat.user?.id ?? 0, + username: chat.user?.username ?? "a", + isOpen: chat.isOpen, + })); + setContacts(contacts); + handleContactSelect(contacts[0]); + } + }, + !!data || isError, + data + ); + + useEffect(() => { + if (socket && contacts) { + socket.on("chatCreated", (chat: Chat) => { + const contact = { + chatId: chat.id, + userId: chat.customerId, + username: chat.user!.username, + isOpen: chat.isOpen, + }; + setContacts([...contacts, contact]); + }); + } + }, [contacts]); + + return { + contacts, + selectedContact, + handleContactSelect, + }; +}; diff --git a/src/app/(admin)/new_dashboard/hooks/useIsChatPopupOpen.ts b/src/app/(admin)/new_dashboard/hooks/useIsChatPopupOpen.ts new file mode 100644 index 0000000..d9b8d5a --- /dev/null +++ b/src/app/(admin)/new_dashboard/hooks/useIsChatPopupOpen.ts @@ -0,0 +1,10 @@ +import { useState } from "react"; + +export const useIsChatPopupOpen = () => { + const [isOpen, setIsOpen] = useState(false); + + return { + isOpen, + setIsOpen, + }; +}; diff --git a/src/app/(admin)/new_dashboard/layout.tsx b/src/app/(admin)/new_dashboard/layout.tsx new file mode 100644 index 0000000..99764b5 --- /dev/null +++ b/src/app/(admin)/new_dashboard/layout.tsx @@ -0,0 +1,59 @@ +"use client"; +import { Container, Box } from "@mui/material"; +import Sidebar from "./shared/sidebar/Sidebar"; +import ContactSidebar from "./components/ContactSidebar"; +import ChatPopup from "./components/ChatPopup"; +import { useChat } from "@/app/hooks/useChat"; +import { useIsDashboardPage } from "./guides/hooks/useIsDashboardPage"; +import { useIsChatPopupOpen } from "./hooks/useIsChatPopupOpen"; +import { useIsCreateGuidePage } from "./guides/hooks/useIsCreateGuidePage"; +import { + LeftSidebarWrapper, + RightSidebarWrapper, + PageWrapper, + MainWrapper, +} from "@/app/wrappers/wrappers"; +import ContactList from "./components/ContactList"; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + const { selectedContact } = useChat(); + const isDashboardPage = useIsDashboardPage(); + const isCreateGuidePage = useIsCreateGuidePage(); + const { isOpen, setIsOpen } = useIsChatPopupOpen(); + + return ( + + + + + + {children} + + + + + {(isOpen && !isDashboardPage) && ( + + )} + {!isDashboardPage && !isCreateGuidePage && ( + + )} + + + ); +} diff --git a/src/app/(admin)/new_dashboard/page.tsx b/src/app/(admin)/new_dashboard/page.tsx new file mode 100644 index 0000000..de8bd8f --- /dev/null +++ b/src/app/(admin)/new_dashboard/page.tsx @@ -0,0 +1,71 @@ +"use client"; +import PageContainer from "@/components/PageContainer"; +import DashboardCard from "./shared/Card"; +import { Box } from "@mui/material"; +import ContactList from "./components/ContactList"; +import ChatHeader from "./components/ChatHeader"; +import SupportMessageList from "./components/SupportMessageList"; +import MessageInput from "@/common/components/MessageInput"; +import { useChat } from "@/app/hooks/useChat"; + +const Page = () => { + const { selectedContact, handleContactSelect, contacts } = useChat(); + + return ( + + + + + + {/* Chat Container */} + {selectedContact && ( + + {/* Chat Header */} + {selectedContact && ( + + )} + + {/* Message List */} + {selectedContact && ( + + )} + + {/* Input and Send Button */} + {selectedContact && selectedContact.isOpen && ( + + )} + + )} + + + + ); +}; + +export default Page; diff --git a/src/app/(admin)/new_dashboard/shared/Card/index.tsx b/src/app/(admin)/new_dashboard/shared/Card/index.tsx new file mode 100644 index 0000000..4439eb5 --- /dev/null +++ b/src/app/(admin)/new_dashboard/shared/Card/index.tsx @@ -0,0 +1,102 @@ +import { Card, CardContent, Typography, Stack, Box } from "@mui/material"; + +type Props = { + title?: string; + subtitle?: string; + action?: JSX.Element | any; + footer?: JSX.Element; + cardheading?: string | JSX.Element; + headtitle?: string | JSX.Element; + headsubtitle?: string | JSX.Element; + children?: JSX.Element; + middlecontent?: string | JSX.Element; + fullHeight?: boolean; + isVisibleBorder?: boolean; + buttons?: JSX.Element; +}; + +const DashboardCard = ({ + title, + subtitle, + children, + action, + footer, + cardheading, + buttons, + headtitle, + headsubtitle, + middlecontent, + fullHeight = false, + isVisibleBorder = false, +}: Props) => { + return ( + + {cardheading ? ( + + {headtitle} + + {headsubtitle} + + + ) : ( + + {title ? ( + + + + {title ? {title} : ""} + + {subtitle ? ( + + {subtitle} + + ) : ( + "" + )} + + {buttons ? ( + + {buttons} + + ) : ( + "" + )} + + + {action} + + ) : null} + {children} + + )} + + {middlecontent} + {footer} + + ); +}; + +export default DashboardCard; diff --git a/src/app/(admin)/new_dashboard/shared/sidebar/NavGroup/index.tsx b/src/app/(admin)/new_dashboard/shared/sidebar/NavGroup/index.tsx new file mode 100644 index 0000000..79496d1 --- /dev/null +++ b/src/app/(admin)/new_dashboard/shared/sidebar/NavGroup/index.tsx @@ -0,0 +1,32 @@ +import PropTypes from "prop-types"; +import { ListSubheader, styled, Theme } from "@mui/material"; + +type NavGroup = { + navlabel?: boolean; + subheader?: string; +}; + +interface ItemType { + item: NavGroup; +} + +const NavGroup = ({ item }: ItemType) => { + const ListSubheaderStyle = styled((props: Theme | any) => ( + + ))(({ theme }) => ({ + ...theme.typography.overline, + fontWeight: "700", + marginTop: theme.spacing(3), + marginBottom: theme.spacing(0), + color: "text.secondary", + lineHeight: "26px", + padding: "3px 12px", + })); + return {item.subheader}; +}; + +NavGroup.propTypes = { + item: PropTypes.object, +}; + +export default NavGroup; diff --git a/src/app/(admin)/new_dashboard/shared/sidebar/NavItem/index.tsx b/src/app/(admin)/new_dashboard/shared/sidebar/NavItem/index.tsx new file mode 100644 index 0000000..5c8403b --- /dev/null +++ b/src/app/(admin)/new_dashboard/shared/sidebar/NavItem/index.tsx @@ -0,0 +1,99 @@ +import { + ListItemIcon, + ListItem, + List, + styled, + ListItemText, + useTheme, + ListItemButton, +} from "@mui/material"; +import Link from "next/link"; + +type NavGroup = { + [x: string]: any; + id?: string; + navlabel?: boolean; + subheader?: string; + title?: string; + icon?: any; + href?: any; + onClick?: () => void; // Adjusted type to a function without event + disabled?: boolean; + external?: boolean; +}; + +interface ItemType { + item: NavGroup; + hideMenu?: any; + level?: number | any; + pathDirect: string; +} + +const NavItem = ({ item, level, pathDirect }: ItemType) => { + const Icon = item.icon; + const theme = useTheme(); + const itemIcon = ; + + const ListItemStyled = styled(ListItem)(() => ({ + padding: 0, + ".MuiButtonBase-root": { + whiteSpace: "nowrap", + marginBottom: "2px", + padding: "8px 10px", + borderRadius: "8px", + backgroundColor: level > 1 ? "transparent !important" : "inherit", + color: theme.palette.text.secondary, + paddingLeft: "10px", + "&:hover": { + backgroundColor: theme.palette.primary.light, + color: theme.palette.primary.main, + }, + "&.Mui-selected": { + color: theme.palette.primary.main, + backgroundColor: theme.palette.primary.light, + "&:hover": { + backgroundColor: theme.palette.primary.light, + color: theme.palette.primary.main, + }, + }, + }, + })); + + const handleClick = (e: React.MouseEvent) => { + if (item.onClick) { + e.preventDefault(); // Prevent default navigation + item.onClick(); + } + }; + + return ( + + + + + {itemIcon} + + + <>{item.title} + + + + + ); +}; + +export default NavItem; diff --git a/src/app/(admin)/new_dashboard/shared/sidebar/Sidebar.tsx b/src/app/(admin)/new_dashboard/shared/sidebar/Sidebar.tsx new file mode 100644 index 0000000..36934bc --- /dev/null +++ b/src/app/(admin)/new_dashboard/shared/sidebar/Sidebar.tsx @@ -0,0 +1,123 @@ +import { Box, Button, Drawer, Typography } from "@mui/material"; +import Image from "next/image"; +import SidebarItems from "./SidebarItems"; +import Link from "next/link"; + +interface ItemType { + isSidebarOpen: boolean; +} + +const DashboardSidebar = ({ isSidebarOpen }: ItemType) => { + const sidebarWidth = "270px"; + + // Custom CSS for short scrollbar + const scrollbarStyles = { + "&::-webkit-scrollbar": { + width: "7px", + }, + "&::-webkit-scrollbar-thumb": { + backgroundColor: "paper", + borderRadius: "15px", + }, + }; + + return ( + + {/* ------------------------------------------- */} + {/* Sidebar*/} + {/* ------------------------------------------- */} + + {/* ------------------------------------------- */} + {/* Sidebar Box */} + {/* ------------------------------------------- */} + + + {/* ------------------------------------------- */} + {/* Logo */} + {/* ------------------------------------------- */} + + logo + + + {/* ------------------------------------------- */} + {/* Sidebar Items */} + {/* ------------------------------------------- */} + + + <> + + + Empower Guides with AI! + + + + chatbot + + + + + + + + + ); +}; +export default DashboardSidebar; diff --git a/src/app/(admin)/new_dashboard/shared/sidebar/SidebarItems.tsx b/src/app/(admin)/new_dashboard/shared/sidebar/SidebarItems.tsx new file mode 100644 index 0000000..d9ac27e --- /dev/null +++ b/src/app/(admin)/new_dashboard/shared/sidebar/SidebarItems.tsx @@ -0,0 +1,34 @@ +import { useMenuItems } from "./hooks/useMenuItems"; +import { usePathname } from "next/navigation"; +import { Box, List } from "@mui/material"; +import NavItem from "./NavItem"; +import NavGroup from "./NavGroup"; + +const SidebarItems = () => { + const pathname = usePathname(); + const pathDirect = pathname; + const { menuItems: Menuitems } = useMenuItems(); + return ( + + + {Menuitems.map((item) => { + // {/********SubHeader**********/} + if (item.subheader) { + return ; + + // {/********If Sub Menu**********/} + } else { + return ( + + ); + } + })} + + + ); +}; +export default SidebarItems; diff --git a/src/app/(admin)/new_dashboard/shared/sidebar/hooks/useMenuItems.ts b/src/app/(admin)/new_dashboard/shared/sidebar/hooks/useMenuItems.ts new file mode 100644 index 0000000..b2d1049 --- /dev/null +++ b/src/app/(admin)/new_dashboard/shared/sidebar/hooks/useMenuItems.ts @@ -0,0 +1,78 @@ +import DashboardIcon from "@mui/icons-material/Dashboard"; +import LogoutIcon from "@mui/icons-material/Logout"; +import { uniqueId } from "lodash"; +import ManageAccountsIcon from "@mui/icons-material/ManageAccounts"; +import AutoStoriesIcon from "@mui/icons-material/AutoStories"; +import { AuthClient } from "@/api/auth.client"; +import { useRouter } from "next/navigation"; +import CreateIcon from "@mui/icons-material/Create"; +import { useGuideContext } from "@/app/providers/guide"; + +const authClient = new AuthClient(); + +export const useMenuItems = () => { + const router = useRouter(); + const { setGuide } = useGuideContext(); + + const handleLogout = async () => { + await authClient.logout(); + router.push("/login"); + }; + + const handleCreateGuide = () => { + setGuide({ + contentHTML: "", + title: "", + }); + router.push("/new_dashboard/guides/create"); + }; + + const menuItems = [ + { + navlabel: true, + subheader: "Home", + }, + { + id: uniqueId(), + title: "Dashboard", + icon: DashboardIcon, + href: "/new_dashboard", + }, + { + navlabel: true, + subheader: "Support", + }, + { + id: uniqueId(), + title: "Guides", + icon: AutoStoriesIcon, + href: "/new_dashboard/guides", + }, + { + id: uniqueId(), + title: "Create Guide", + icon: CreateIcon, + href: "/new_dashboard/guides/create", + onClick: handleCreateGuide, + }, + { + navlabel: true, + subheader: "Auth", + }, + { + id: uniqueId(), + title: "Logout", + icon: LogoutIcon, + onClick: handleLogout, + href: "#", + }, + { + id: uniqueId(), + title: "Manage Accounts", + icon: ManageAccountsIcon, + href: "#", + }, + ]; + + return { menuItems }; +}; diff --git a/src/app/(admin)/new_dashboard/types/contact.ts b/src/app/(admin)/new_dashboard/types/contact.ts new file mode 100644 index 0000000..2792105 --- /dev/null +++ b/src/app/(admin)/new_dashboard/types/contact.ts @@ -0,0 +1,6 @@ +export type Contact = { + username: string; + chatId: number; + isOpen: boolean; + userId: number; +}; diff --git a/src/app/(admin)/new_dashboard/types/index.ts b/src/app/(admin)/new_dashboard/types/index.ts new file mode 100644 index 0000000..f3b6f73 --- /dev/null +++ b/src/app/(admin)/new_dashboard/types/index.ts @@ -0,0 +1 @@ +export * from "./contact"; diff --git a/src/app/(public)/login/hooks/useLoginForm.ts b/src/app/(public)/login/hooks/useLoginForm.ts new file mode 100644 index 0000000..731a3c0 --- /dev/null +++ b/src/app/(public)/login/hooks/useLoginForm.ts @@ -0,0 +1,49 @@ +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { schema } from "../validations/schema"; +import { LoginRequest } from "@/api/types/login"; +import { useRouter } from "next/navigation"; +import { useLogin } from "@/hooks"; +import { SuccessResponse } from "@/types"; +import { UserRole } from "@/api/types/User"; + +const useLoginForm = () => { + const { + register, + handleSubmit, + formState: { errors, isValid }, + } = useForm({ + resolver: zodResolver(schema), + mode: "onChange", + reValidateMode: "onChange", + }); + + const router = useRouter(); + + const { mutate, isError, error, isPending } = useLogin(); + + const onSubmit = (data: LoginRequest) => { + mutate(data, { + onSuccess: (response) => { + const { data: user } = response as SuccessResponse; + if (user.roles.includes("admin")) { + router.push("/new_dashboard"); + } else { + router.push("/new_chat/" + user.id); + } + }, + }); + }; + + return { + register, + handleSubmit: handleSubmit(onSubmit), + errors, + isValid, + isError, + isPending, + error, + }; +}; + +export default useLoginForm; diff --git a/src/app/(public)/login/page.tsx b/src/app/(public)/login/page.tsx new file mode 100644 index 0000000..935101a --- /dev/null +++ b/src/app/(public)/login/page.tsx @@ -0,0 +1,142 @@ +"use client"; + +import Avatar from "@mui/material/Avatar"; +import TextField from "@mui/material/TextField"; +import Link from "@mui/material/Link"; +import Paper from "@mui/material/Paper"; +import Box from "@mui/material/Box"; +import Grid from "@mui/material/Grid"; +import LockOutlinedIcon from "@mui/icons-material/LockOutlined"; +import Typography from "@mui/material/Typography"; +import { Alert, Backdrop, CircularProgress } from "@mui/material"; +import useLoginForm from "./hooks/useLoginForm"; +import { LoadingButton } from "@mui/lab"; +import PageContainer from "@/components/PageContainer"; + +const Login = () => { + const { register, handleSubmit, errors, isValid, isError, error, isPending } = + useLoginForm(); + + return ( + <> + ({ color: "#fff", zIndex: theme.zIndex.drawer + 1 })} + open={isPending} + > + + + + + + t.palette.mode === "light" + ? t.palette.grey[50] + : t.palette.grey[900], + backgroundSize: "cover", + backgroundPosition: "left", + }} + /> + + + + + + + Sign in + + {isError && ( + + {error?.message} + + )} + + + + + Sign In + + + + + {"Don't have an account? Sign Up"} + + + + + + + + + + ); +}; + +export default Login; diff --git a/src/app/(public)/login/validations/schema.ts b/src/app/(public)/login/validations/schema.ts new file mode 100644 index 0000000..34357d8 --- /dev/null +++ b/src/app/(public)/login/validations/schema.ts @@ -0,0 +1,6 @@ +import { z } from "zod"; + +export const schema = z.object({ + username: z.string().min(6, "Invalid username"), + password: z.string().min(6, "Password must be at least 6 characters long"), +}); diff --git a/src/app/(public)/signup/hooks/useSignUpForm.ts b/src/app/(public)/signup/hooks/useSignUpForm.ts new file mode 100644 index 0000000..f75b5a4 --- /dev/null +++ b/src/app/(public)/signup/hooks/useSignUpForm.ts @@ -0,0 +1,42 @@ +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { schema } from "../validations/schema"; +import { useRouter } from "next/navigation"; +import { useSignUp } from "@/hooks"; +import { SignUpRequest } from "@/api/types/signup"; + +const useSignUpForm = () => { + const { + register, + handleSubmit, + formState: { errors, isValid }, + } = useForm({ + resolver: zodResolver(schema), + mode: "onChange", + reValidateMode: "onChange", + }); + + const router = useRouter(); + + const { mutate, isError, error, isPending } = useSignUp(); + + const onSubmit = (data: SignUpRequest) => { + mutate(data, { + onSuccess: () => { + router.push("/login"); + }, + }); + }; + + return { + register, + handleSubmit: handleSubmit(onSubmit), + errors, + isValid, + isError, + isPending, + error, + }; +}; + +export default useSignUpForm; diff --git a/src/app/(public)/signup/page.tsx b/src/app/(public)/signup/page.tsx new file mode 100644 index 0000000..e3f3830 --- /dev/null +++ b/src/app/(public)/signup/page.tsx @@ -0,0 +1,131 @@ +"use client"; + +import Avatar from "@mui/material/Avatar"; +import TextField from "@mui/material/TextField"; +import Link from "@mui/material/Link"; +import Grid from "@mui/material/Grid"; +import Box from "@mui/material/Box"; +import LockOutlinedIcon from "@mui/icons-material/LockOutlined"; +import Typography from "@mui/material/Typography"; +import Container from "@mui/material/Container"; +import { Paper, Alert } from "@mui/material"; +import useSignUpForm from "./hooks/useSignUpForm"; +import { LoadingButton } from "@mui/lab"; +import PageContainer from "@/components/PageContainer"; + +const SignUp = () => { + const { register, handleSubmit, errors, isValid, isError, error, isPending } = + useSignUpForm(); + + return ( + + + + + + + + Sign up + + {isError && ( + + {error?.message} + + )} + + + + + + + + + + + + + + Sign Up + + + + + Already have an account? Sign in + + + + + + + + ); +}; + +export default SignUp; diff --git a/src/app/(public)/signup/validations/schema.ts b/src/app/(public)/signup/validations/schema.ts new file mode 100644 index 0000000..cd10a13 --- /dev/null +++ b/src/app/(public)/signup/validations/schema.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const schema = z.object({ + username: z.string().min(6, "Invalid username"), + email: z.string().email("Invalid email"), + password: z.string().min(6, "Password must be at least 6 characters long"), +}); diff --git a/src/app/(user)/new_chat/[userId]/components/UserMessageList/index.tsx b/src/app/(user)/new_chat/[userId]/components/UserMessageList/index.tsx new file mode 100644 index 0000000..e84e054 --- /dev/null +++ b/src/app/(user)/new_chat/[userId]/components/UserMessageList/index.tsx @@ -0,0 +1,55 @@ +import { Box, List } from "@mui/material"; +import { useSocket } from "@/app/hooks/useSocket"; +import { useMessageList } from "@/common/hooks/useMessageList"; +import { MessageContainer } from "@/common"; + +type Props = { + chatId: number; +}; + +const UserMessageList = ({ chatId }: Props) => { + const socket = useSocket(); + const { messages } = useMessageList({ chatId, socket }); + + return ( + + + {messages && + Object.keys(messages).map((date, index) => { + return ( + + + {date} + + {messages[date].map((msg, index) => { + return ( + !msg.isNote && ( + + ) + ); + })} + + ); + })} + + + ); +}; + +export default UserMessageList; diff --git a/src/app/(user)/new_chat/[userId]/hooks/index.ts b/src/app/(user)/new_chat/[userId]/hooks/index.ts new file mode 100644 index 0000000..e3f97a2 --- /dev/null +++ b/src/app/(user)/new_chat/[userId]/hooks/index.ts @@ -0,0 +1 @@ +export * from "./useChat"; diff --git a/src/app/(user)/new_chat/[userId]/hooks/useChat.ts b/src/app/(user)/new_chat/[userId]/hooks/useChat.ts new file mode 100644 index 0000000..811a89e --- /dev/null +++ b/src/app/(user)/new_chat/[userId]/hooks/useChat.ts @@ -0,0 +1,34 @@ +import { useEffect, useState } from "react"; +import { useChatByUserId } from "@/hooks/api/chatHooks"; +import { Socket } from "socket.io-client"; + +type Props = { + userId: number; + socket: Socket; +}; +type Chat = { + id: number; +}; + +export const useChat = ({ userId, socket }: Props) => { + const [chatId, setChatId] = useState(null); + const { data: chat, error, isPending } = useChatByUserId(userId); + + useEffect(() => { + if (!isPending) { + if (chat == null || chat.user?.id !== userId) { + socket.emit("create"); + socket.on("chatCreated", (chat: Chat) => { + socket.emit("join", { chatId: chat.id }); + setChatId(chat.id); + }); + return; + } + if (chat.id) { + socket.emit("join", { chatId: chat.id }); + setChatId(chat.id); + } + } + }, [isPending]); + return { chatId, error }; +}; diff --git a/src/app/(user)/new_chat/[userId]/page.tsx b/src/app/(user)/new_chat/[userId]/page.tsx new file mode 100644 index 0000000..4e36519 --- /dev/null +++ b/src/app/(user)/new_chat/[userId]/page.tsx @@ -0,0 +1,56 @@ +"use client"; +import PageContainer from "@/components/PageContainer"; +import { Box, Card } from "@mui/material"; +import { useChat } from "./hooks"; +import { useParams } from "next/navigation"; +import { useSocket } from "@/app/hooks/useSocket"; +import MessageInput from "@/common/components/MessageInput"; +import UserMessageList from "./components/UserMessageList"; + +const ChatPage = () => { + const { userId } = useParams<{ userId: string }>()!; + const { chatId, error } = useChat({ + userId: parseInt(userId), + socket: useSocket(), + }); + + return ( + + + + {chatId ? ( + <> + + + + ) : null} + {error ?

{error.message}

: null} +
+
+
+ ); +}; +export default ChatPage; diff --git a/src/app/favicon.ico b/src/app/favicon.ico deleted file mode 100644 index 718d6fe..0000000 Binary files a/src/app/favicon.ico and /dev/null differ diff --git a/src/app/globals.css b/src/app/globals.css index 875c01e..dfa9b50 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2,32 +2,84 @@ @tailwind components; @tailwind utilities; -:root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; -} - -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - } -} - -body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); -} - -@layer utilities { - .text-balance { - text-wrap: balance; - } + +.quill-content h1 { + @apply text-2xl font-bold mb-4 text-gray-800; +} + +.quill-content h2 { + @apply text-xl font-bold mb-3 text-gray-700; +} + +.quill-content h3 { + @apply text-lg font-bold mb-3 text-gray-600; +} + +.quill-content h4 { + @apply text-base font-bold mb-2 text-gray-600; +} + +.quill-content p { + @apply text-base mb-4 text-gray-600; +} + +.quill-content ul { + @apply list-disc ml-5 mb-4; +} + +.quill-content ul li { + @apply mb-2 text-base text-gray-600; +} + +.quill-content ol { + @apply list-decimal ml-5 mb-4; +} + +.quill-content ol li { + @apply mb-2 text-base text-gray-600; +} + +.quill-content img { + @apply max-w-full h-auto my-4 rounded-lg; +} + +.quill-content blockquote { + @apply my-4 p-4 bg-gray-100 border-l-4 border-gray-300 italic text-gray-600; +} + +.quill-content a { + @apply text-blue-600 underline; +} + +.quill-content a:hover { + @apply text-blue-500; +} + +.quill-content pre { + @apply bg-gray-100 rounded p-4 overflow-x-auto font-mono text-sm text-gray-700 mb-4; +} + +.quill-content code { + @apply bg-gray-100 rounded p-1 font-mono text-sm text-red-600; +} + +.quill-content hr { + @apply border-t border-gray-200 my-6; +} + +.quill-content table { + @apply w-full border-collapse mb-4; +} + +.quill-content table th, +.quill-content table td { + @apply border border-gray-300 p-2 text-left; +} + +.quill-content table th { + @apply bg-gray-100 text-gray-800; +} + +.quill-content table td { + @apply bg-white text-gray-700; } diff --git a/src/app/hooks/useChat.ts b/src/app/hooks/useChat.ts new file mode 100644 index 0000000..0117c9d --- /dev/null +++ b/src/app/hooks/useChat.ts @@ -0,0 +1,12 @@ +'use client'; +import { useContext } from 'react'; +import { ChatContextValue } from '../providers/ChatProvider/provider'; +import { ChatContext } from '../providers/ChatProvider/provider'; + +export const useChat = (): ChatContextValue => { + const context = useContext(ChatContext); + if (!context) { + throw new Error('useChat must be used within a ChatProvider'); + } + return context; +}; diff --git a/src/app/hooks/useSocket.ts b/src/app/hooks/useSocket.ts new file mode 100644 index 0000000..7027c87 --- /dev/null +++ b/src/app/hooks/useSocket.ts @@ -0,0 +1,11 @@ +import { useContext } from 'react'; +import { Socket } from 'socket.io-client'; +import { SocketContext } from '../providers/SocketProvider/provider'; + +export const useSocket = (): Socket => { + const context = useContext(SocketContext); + if (!context) { + throw new Error('useSocket must be used within a SocketProvider'); + } + return context; +}; diff --git a/src/app/icon.ico b/src/app/icon.ico new file mode 100644 index 0000000..c738b99 Binary files /dev/null and b/src/app/icon.ico differ diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 3314e47..3224f07 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,13 +1,12 @@ -import type { Metadata } from "next"; -import { Inter } from "next/font/google"; -import "./globals.css"; - -const inter = Inter({ subsets: ["latin"] }); +"use client"; -export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", -}; +import { baselightTheme } from "@/util/theme/theme"; +import "./globals.css"; +import { SocketProvider } from "./providers/SocketProvider/provider"; +import { QueryProvider } from "./providers/QueryProvider/provider"; +import { CssBaseline, ThemeProvider } from "@mui/material"; +import { ChatProvider } from "./providers/ChatProvider/provider"; +import { GuideProvider } from "./providers/guide"; export default function RootLayout({ children, @@ -16,7 +15,18 @@ export default function RootLayout({ }>) { return ( - {children} + + + + + + + {children} + + + + + ); } diff --git a/src/app/providers/ChatProvider/hooks/useContacts.ts b/src/app/providers/ChatProvider/hooks/useContacts.ts new file mode 100644 index 0000000..fd5a2af --- /dev/null +++ b/src/app/providers/ChatProvider/hooks/useContacts.ts @@ -0,0 +1,83 @@ +"use client"; +import { useEffect, useState } from "react"; +import { ClientResponse, SuccessResponse } from "@/types"; +import { useChats } from "@/hooks/api/chatHooks"; +import { useOnFetch } from "@/common/hooks/useOnFetch"; +import { Socket } from "socket.io-client"; +import { Contact } from "@/app/(admin)/new_dashboard/types"; +import { Chat } from "@/api/types/chat"; + +type Props = { + socket: Socket; +}; + +export const useContacts = ({ socket }: Props) => { + const [contacts, setContacts] = useState([]); + const { isError, data, error } = useChats(); + const [selectedContact, setSelectedContact] = useState(null); + + useEffect(() => { + if (socket && selectedContact && !selectedContact.isOpen) { + setContacts( + contacts.map((contact) => + contact.chatId === selectedContact.chatId + ? { ...contact, isOpen: false } + : contact + ) + ); + return; + } + if (socket && selectedContact) { + socket.emit("join", { chatId: selectedContact.chatId }); + return () => { + socket.emit("leave", { chatId: selectedContact.chatId }); + }; + } + }, [selectedContact]); + + const handleContactSelect = (contact: Contact) => { + console.log(contact); + setSelectedContact(contact); + }; + + useOnFetch( + (clientResponse: ClientResponse) => { + if (isError) { + throw error; + } else { + const successResponse = clientResponse as SuccessResponse; + const chats = successResponse.data; + const contacts = chats.map((chat) => ({ + chatId: chat.id, + userId: chat.user?.id ?? 0, + username: chat.user?.username ?? "a", + isOpen: chat.isOpen, + })); + setContacts(contacts); + handleContactSelect(contacts[0]); + } + }, + !!data || isError, + data + ); + + useEffect(() => { + if (socket && contacts) { + socket.on("chatCreated", (chat: Chat) => { + const contact = { + chatId: chat.id, + userId: chat.customerId, + username: chat.user!.username, + isOpen: chat.isOpen, + }; + setContacts([...contacts, contact]); + }); + } + }, [contacts]); + + return { + contacts, + selectedContact, + handleContactSelect, + }; +}; diff --git a/src/app/providers/ChatProvider/provider.tsx b/src/app/providers/ChatProvider/provider.tsx new file mode 100644 index 0000000..d074552 --- /dev/null +++ b/src/app/providers/ChatProvider/provider.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { Contact } from "@/app/(admin)/new_dashboard/types"; +import { useSocket } from "@/app/hooks/useSocket"; +import React, { createContext, useContext, ReactNode } from "react"; +import { useContacts } from "./hooks/useContacts"; + +interface ChatProviderProps { + children: ReactNode; +} + +export interface ChatContextValue { + selectedContact: Contact | null; + handleContactSelect: (contact: Contact) => void; + contacts: Contact[]; +} + +export const ChatContext = createContext(null); + +export const ChatProvider: React.FC = ({ children }) => { + const socket = useSocket(); + const { contacts, selectedContact, handleContactSelect } = useContacts({ + socket, + }); + return ( + + {children} + + ); +}; + +export const useGlobalChatContext = () => useContext(ChatContext); diff --git a/src/app/providers/QueryProvider/provider.tsx b/src/app/providers/QueryProvider/provider.tsx new file mode 100644 index 0000000..e250536 --- /dev/null +++ b/src/app/providers/QueryProvider/provider.tsx @@ -0,0 +1,11 @@ +"use client" + +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +export const queryClient = new QueryClient(); + +export function QueryProvider({ children }: { children: React.ReactNode }) { + return ( + {children} + ); +} \ No newline at end of file diff --git a/src/app/providers/SocketProvider/provider.tsx b/src/app/providers/SocketProvider/provider.tsx new file mode 100644 index 0000000..fc5d707 --- /dev/null +++ b/src/app/providers/SocketProvider/provider.tsx @@ -0,0 +1,24 @@ +"use client"; + +import React, { createContext, ReactNode, useMemo } from "react"; +import socket from "@/socket"; +import { Socket } from "socket.io-client"; + +export const SocketContext = createContext(null); + +interface SocketProviderProps { + children: ReactNode; +} + +export const SocketProvider: React.FC = ({ children }) => { + const socketInstance = useMemo(() => { + socket.connect(); + return socket; + }, []); + + return ( + + {children} + + ); +}; diff --git a/src/app/providers/guide.tsx b/src/app/providers/guide.tsx new file mode 100644 index 0000000..12fcc1a --- /dev/null +++ b/src/app/providers/guide.tsx @@ -0,0 +1,37 @@ +import { + createContext, + Dispatch, + SetStateAction, + useContext, + useState, +} from "react"; + +interface GuideContextType { + guide: Guide | undefined; + setGuide: Dispatch>; +} + +type Guide = { + title: string; + contentHTML: string; +}; + +export const guideContext = createContext( + undefined +); + +export const GuideProvider = ({ children }: { children: React.ReactNode }) => { + const [guide, setGuide] = useState(undefined); + return ( + + {children} + + ); +}; +export const useGuideContext = () => { + const context = useContext(guideContext); + if (!context) { + throw new Error("useGuideContext must be used within a GuideProvider"); + } + return context; +}; diff --git a/src/app/wrappers/wrappers.ts b/src/app/wrappers/wrappers.ts new file mode 100644 index 0000000..1588b2c --- /dev/null +++ b/src/app/wrappers/wrappers.ts @@ -0,0 +1,29 @@ +import { styled, Box } from "@mui/material"; + +export const MainWrapper = styled("div")(() => ({ + display: "flex", + minHeight: "100vh", + width: "100%", +})); + +export const PageWrapper = styled("div")(() => ({ + display: "flex", + flexGrow: 1, + flexDirection: "column", + zIndex: 1, + backgroundColor: "transparent", +})); + +export const LeftSidebarWrapper = styled(Box)(() => ({ + display: "flex", + flexDirection: "column", + alignItems: "flex-start", + gap: 2, +})); + +export const RightSidebarWrapper = styled(Box)(() => ({ + display: "flex", + flexDirection: "column", + alignItems: "flex-end", + gap: 2, + })); \ No newline at end of file diff --git a/src/assets/images/logo_google_icon.ico b/src/assets/images/logo_google_icon.ico new file mode 100644 index 0000000..f17a0a7 Binary files /dev/null and b/src/assets/images/logo_google_icon.ico differ diff --git a/src/common/components/Message/index.tsx b/src/common/components/Message/index.tsx new file mode 100644 index 0000000..a6db227 --- /dev/null +++ b/src/common/components/Message/index.tsx @@ -0,0 +1,83 @@ +import { ListItem, Box, Typography } from "@mui/material"; +import { Message as MessageType } from "@/api/types/message"; + +type Props = { + message: MessageType; + isSupport?: boolean; + username?: string; +}; + +export const MessageContainer = (messageProps: Props) => { + const { username, message, isSupport = true } = messageProps; + const { isNote, isSupportSender, timeStamp, content } = message; + const chatMode = isSupport ? isSupportSender : !isSupportSender; + return ( + + theme.palette.lightBlue.main + : (theme) => theme.palette.note.main + : (theme) => theme.palette.lightGrey.main, + borderRadius: 2, + padding: 1, + maxWidth: "70%", + wordWrap: "break-word", + }} + > + + {isSupport + ? !isSupportSender + ? username + : "" + : isSupportSender + ? "Support" + : ""} + + + {content} + + theme.palette.lightGrey.light} + sx={{ + alignSelf: chatMode ? "flex-end" : "flex-start", + }} + > + {new Date(timeStamp).toLocaleTimeString("il-IL", { + hour: "2-digit", + minute: "2-digit", + hour12: false, + })} + + + + ); +}; diff --git a/src/common/components/MessageInput/hooks/useMessage.ts b/src/common/components/MessageInput/hooks/useMessage.ts new file mode 100644 index 0000000..5807369 --- /dev/null +++ b/src/common/components/MessageInput/hooks/useMessage.ts @@ -0,0 +1,57 @@ +"use client"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { schema } from "../validations/schema"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Socket } from "socket.io-client"; + +type MessageRequest = { + message: string; +}; +type Props = { + chatId: number; + socket: Socket; + isSupport: boolean; +}; + +export const useMessage = ({ chatId, socket, isSupport }: Props) => { + const [isNote, setIsNote] = useState(false); + + const { + register, + handleSubmit, + reset, + formState: { errors }, + } = useForm({ + resolver: zodResolver(schema), + mode: "onChange", + reValidateMode: "onChange", + }); + + const handleSendMessage = (messageRequest: MessageRequest) => { + const newMessage = messageRequest.message; + const data = { + chatId: chatId, + content: newMessage, + }; + const allMessage = { + data: data, + isSupportSender: isSupport, + isNote: isNote, + }; + + if (chatId) { + socket.emit("message", allMessage); + reset({ + message: "", + }); + } + }; + return { + isNote, + handleChangeNote: () => setIsNote(!isNote), + register, + handleSubmit: handleSubmit(handleSendMessage), + errors, + }; +}; diff --git a/src/common/components/MessageInput/index.tsx b/src/common/components/MessageInput/index.tsx new file mode 100644 index 0000000..89e5736 --- /dev/null +++ b/src/common/components/MessageInput/index.tsx @@ -0,0 +1,88 @@ +import { Box, TextField, IconButton, Button } from "@mui/material"; +import SendIcon from "@mui/icons-material/Send"; +import { useMessage } from "./hooks/useMessage"; +import { useSocket } from "@/app/hooks/useSocket"; + +type MessageInputProps = { + chatId: number; + isSupport?: boolean; + isPopup?: boolean; +}; + +const MessageInput = ({ + chatId, + isSupport = true, + isPopup = false, +}: MessageInputProps) => { + const socket = useSocket(); + + const { errors, handleChangeNote, handleSubmit, isNote, register } = + useMessage({ chatId, socket, isSupport }); + + return ( + + {isSupport && ( + + + + )} + + + + + + + + + ); +}; + +export default MessageInput; diff --git a/src/common/components/MessageInput/validations/schema.ts b/src/common/components/MessageInput/validations/schema.ts new file mode 100644 index 0000000..0ad8c04 --- /dev/null +++ b/src/common/components/MessageInput/validations/schema.ts @@ -0,0 +1,11 @@ +import { z } from "zod"; + +export const schema = z.object({ + message: z + .string() + .trim() + .min(1) + .refine((msg) => msg.split("\n").length <= 10, { + message: "Message should not exceed 10 lines", + }), +}); diff --git a/src/common/components/index.ts b/src/common/components/index.ts new file mode 100644 index 0000000..9611b59 --- /dev/null +++ b/src/common/components/index.ts @@ -0,0 +1 @@ +export * from "./Message"; diff --git a/src/common/config/config.ts b/src/common/config/config.ts index 8020f3f..a460032 100644 --- a/src/common/config/config.ts +++ b/src/common/config/config.ts @@ -1 +1,2 @@ -export const SERVER_URL = process.env.SERVER_URL || "http://localhost:8080"; +export const SERVER_URL = + process.env.NEXT_PUBLIC_SERVER_URL || "http://localhost:8080"; diff --git a/src/common/hooks/useMessageList.ts b/src/common/hooks/useMessageList.ts new file mode 100644 index 0000000..71df4d3 --- /dev/null +++ b/src/common/hooks/useMessageList.ts @@ -0,0 +1,53 @@ +"use client"; +import { Chat } from "@/api/types/chat"; +import { Message } from "@/api/types/message"; +import { useOnFetch } from "@/common/hooks/useOnFetch"; +import { useChatById } from "@/hooks/api/chatHooks"; +import { ClientResponse, SuccessResponse } from "@/types"; + +import { useEffect, useState } from "react"; +import { Socket } from "socket.io-client"; +import { groupByDay } from "../../util"; + +type Props = { + chatId: number; + socket: Socket; +}; + +export const useMessageList = ({ chatId, socket }: Props) => { + const [chatMessages, setChatMessages] = useState([]); + const [newMessages, setNewMessages] = useState([]); + const { data, error, isError } = useChatById(chatId); + + useOnFetch( + (clientResponse: ClientResponse) => { + if (isError) { + throw error; + } else { + const successResponse = clientResponse as SuccessResponse; + const messages = successResponse.data.messages; + setChatMessages(messages!); + } + }, + !!data || isError, + data, + false + ); + + useEffect(() => { + socket.on("newMessage", (message: Message) => { + setNewMessages((prev) => [...prev, message]); + }); + + return () => { + socket.off("newMessage"); + }; + }, []); + useEffect(() => { + if (chatId) setNewMessages([]); + }, [chatId]); + + return { + messages: groupByDay(chatMessages.concat(newMessages)), + }; +}; diff --git a/src/common/hooks/useOnFetch.ts b/src/common/hooks/useOnFetch.ts new file mode 100644 index 0000000..e4d8123 --- /dev/null +++ b/src/common/hooks/useOnFetch.ts @@ -0,0 +1,17 @@ +import { useEffect, useRef } from "react"; + +export const useOnFetch = ( + onFetched: (data: T) => void, + isFetched: boolean, + data: T | undefined, + fetchOnce: boolean = true +) => { + const hasFetched = useRef(false); + + useEffect(() => { + if (isFetched && data && !hasFetched.current) { + onFetched(data); + hasFetched.current = fetchOnce; + } + }, [data, isFetched, onFetched]); +}; diff --git a/src/common/index.ts b/src/common/index.ts index ba3dec5..31f218e 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -1,2 +1,3 @@ export * from "./api"; export * from "./config"; +export * from "./components"; diff --git a/src/components/PageContainer/index.tsx b/src/components/PageContainer/index.tsx new file mode 100644 index 0000000..253ee04 --- /dev/null +++ b/src/components/PageContainer/index.tsx @@ -0,0 +1,21 @@ +import { Helmet, HelmetProvider } from "react-helmet-async"; + +type Props = { + description?: string; + children: JSX.Element | JSX.Element[]; + title?: string; +}; + +const PageContainer = ({ title, description, children }: Props) => ( + +
+ + {title} + + + {children} +
+
+); + +export default PageContainer; diff --git a/src/components/Sidebar/Sidebar.scss b/src/components/Sidebar/Sidebar.scss index 402f30b..de8a21a 100644 --- a/src/components/Sidebar/Sidebar.scss +++ b/src/components/Sidebar/Sidebar.scss @@ -55,3 +55,16 @@ .active { color: #fff !important; } +.bg-gradient-primary { + background-color: #4e73df !important; + background-image: linear-gradient(180deg, #4e73df 10%, #224abe 100%); + background-size: cover; +} + +.bg-logout { + background-color: #e05b66 !important; + display: flex; + justify-content: center !important; + align-items: center !important; + // background-image: linear-gradient(180deg, #f8d7da 10%, #f8d7da 100%); +} diff --git a/src/components/Sidebar/Sidebar.tsx b/src/components/Sidebar/Sidebar.tsx index 9606f90..361feb9 100644 --- a/src/components/Sidebar/Sidebar.tsx +++ b/src/components/Sidebar/Sidebar.tsx @@ -3,16 +3,27 @@ import "./Sidebar.scss"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faLaughWink, + faSignOut, faTachometerAlt, faUser, } from "@fortawesome/free-solid-svg-icons"; +import { apiRequest } from "@/common"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; const Sidebar: React.FC = () => { + const router = useRouter(); + + async function handleLogout(): Promise { + console.log("Logging out"); + await apiRequest("/auth/logout", "POST"); + router.push("/login"); // Client-side redirect + } return ( -