diff --git a/.github/ISSUE_TEMPLATE/individual-feature.md b/.github/ISSUE_TEMPLATE/individual-feature.md index 0736148092..43059d4f53 100644 --- a/.github/ISSUE_TEMPLATE/individual-feature.md +++ b/.github/ISSUE_TEMPLATE/individual-feature.md @@ -1,11 +1,11 @@ --- name: Individual Feature -about: - This template helps you to create individual features within a high-level requirement. +about: This template helps you to create individual features within a high-level requirement. This is equivalent to a Feature within an Epic. -title: '[REQ ID] [FEAT ID]' +title: "[REQ ID] [FEAT ID]" labels: chore.Feature assignees: '' + --- # [Feature Name] @@ -16,14 +16,8 @@ assignees: '' Enter some information about the feature you are specifying. -## Functional Requirements - -1. [Link to FR 1]() -2. [Link to FR 2]() - ... - -## Non-Functional Requirements +## Features -1. [Link to NFR 1]() -2. [Link to NFR 2]() - ... +1. [Link to Feature 1]() +2. [Link to Feature 2]() +... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..3ec544c7a4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +.env \ No newline at end of file diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json new file mode 100644 index 0000000000..3f31950765 --- /dev/null +++ b/node_modules/.package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "cs3219-ay2425s1-project-g05", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000000..3f31950765 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "cs3219-ay2425s1-project-g05", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/package.json b/package.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/package.json @@ -0,0 +1 @@ +{} diff --git a/peer-prep/.gitignore b/peer-prep/.gitignore index a547bf36d8..314cd3319f 100644 --- a/peer-prep/.gitignore +++ b/peer-prep/.gitignore @@ -22,3 +22,6 @@ dist-ssr *.njsproj *.sln *.sw? + +.env +.env.local diff --git a/peer-prep/.vite/deps_temp_16910cd1/package.json b/peer-prep/.vite/deps_temp_16910cd1/package.json new file mode 100644 index 0000000000..3dbc1ca591 --- /dev/null +++ b/peer-prep/.vite/deps_temp_16910cd1/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/peer-prep/README.md b/peer-prep/README.md index 74872fd4af..780c92d8b4 100644 --- a/peer-prep/README.md +++ b/peer-prep/README.md @@ -18,11 +18,11 @@ export default tseslint.config({ languageOptions: { // other options... parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], + project: ["./tsconfig.node.json", "./tsconfig.app.json"], tsconfigRootDir: import.meta.dirname, }, }, -}) +}); ``` - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` @@ -31,11 +31,11 @@ export default tseslint.config({ ```js // eslint.config.js -import react from 'eslint-plugin-react' +import react from "eslint-plugin-react"; export default tseslint.config({ // Set the react version - settings: { react: { version: '18.3' } }, + settings: { react: { version: "18.3" } }, plugins: { // Add the react plugin react, @@ -44,7 +44,7 @@ export default tseslint.config({ // other rules... // Enable its recommended rules ...react.configs.recommended.rules, - ...react.configs['jsx-runtime'].rules, + ...react.configs["jsx-runtime"].rules, }, -}) +}); ``` diff --git a/peer-prep/index.html b/peer-prep/index.html index e4b78eae12..e0d1c84080 100644 --- a/peer-prep/index.html +++ b/peer-prep/index.html @@ -1,4 +1,4 @@ - + diff --git a/peer-prep/package-lock.json b/peer-prep/package-lock.json index 06bce73901..839a6e5d84 100644 --- a/peer-prep/package-lock.json +++ b/peer-prep/package-lock.json @@ -10,8 +10,11 @@ "dependencies": { "@mantine/core": "^7.12.2", "@mantine/hooks": "^7.12.2", + "@mantine/notifications": "^7.13.0", + "@tabler/icons-react": "^3.17.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-quill": "^2.0.0", "react-router-dom": "^6.26.2" }, "devDependencies": { @@ -28,7 +31,8 @@ "postcss-simple-vars": "^7.0.1", "typescript": "^5.5.3", "typescript-eslint": "^8.0.1", - "vite": "^5.4.1" + "vite": "^5.4.1", + "vite-plugin-mkcert": "^1.17.6" } }, "node_modules/@ampproject/remapping": { @@ -1019,10 +1023,9 @@ } }, "node_modules/@mantine/core": { - "version": "7.12.2", - "resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.12.2.tgz", - "integrity": "sha512-FrMHOKq4s3CiPIxqZ9xnVX7H4PEGNmbtHMvWO/0YlfPgoV0Er/N/DNJOFW1ys4WSnidPTayYeB41riyxxGOpRQ==", - "license": "MIT", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.13.0.tgz", + "integrity": "sha512-aQpx3Q69ATDhVopBNkWS0sql93ZaPqeA5jTgqU7GxZvJdkpG87vbKYgp4cDV/gqr7BYu4kel0smeHYuPemiZ8Q==", "dependencies": { "@floating-ui/react": "^0.26.9", "clsx": "^2.1.1", @@ -1032,16 +1035,38 @@ "type-fest": "^4.12.0" }, "peerDependencies": { - "@mantine/hooks": "7.12.2", + "@mantine/hooks": "7.13.0", "react": "^18.2.0", "react-dom": "^18.2.0" } }, "node_modules/@mantine/hooks": { - "version": "7.12.2", - "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.12.2.tgz", - "integrity": "sha512-dVMw8jpM0hAzc8e7/GNvzkk9N0RN/m+PKycETB3H6lJGuXJJSRR4wzzgQKpEhHwPccktDpvb4rkukKDq2jA8Fg==", - "license": "MIT", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.13.0.tgz", + "integrity": "sha512-oQpwSi0gajH3UR1DFa9MQ+zeYy75xbc8Im9jIIepLbiJXtIcPK+yll1BMxNwPQLYU1pYI6ZgUazI2PoykVNmsg==", + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/@mantine/notifications": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-7.13.0.tgz", + "integrity": "sha512-EEgKFxUK/4s2FKTYb6hKFbcsLwnccSe/8GsJ18DWGiLRxFq9DsgWCzfPAe+PVcGTITYfK7nbKxEF5l+KLdPOtQ==", + "dependencies": { + "@mantine/store": "7.13.0", + "react-transition-group": "4.4.5" + }, + "peerDependencies": { + "@mantine/core": "7.13.0", + "@mantine/hooks": "7.13.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + } + }, + "node_modules/@mantine/store": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@mantine/store/-/store-7.13.0.tgz", + "integrity": "sha512-ldYJGMcmqTxffQMCQZZWNtXKlG649S3BGM8ukeZ6FLZckVXLQAR2o+G5EkETNihh0sJKR7DVsYHltL5hyxYLkg==", "peerDependencies": { "react": "^18.2.0" } @@ -1084,6 +1109,161 @@ "node": ">= 8" } }, + "node_modules/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "dev": true, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.0.tgz", + "integrity": "sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg==", + "dev": true, + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.3.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.5.tgz", + "integrity": "sha512-ekqR4/+PCLkEBF6qgj8WqJfvDq65RH85OAgrtnVp1mSxaXF03u2xW/hUdweGS5654IlC0wkNYC18Z50tSYTAFw==", + "dev": true, + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.0.tgz", + "integrity": "sha512-r+oZUH7aMFui1ypZnAvZmn0KSqAUgE1/tUXIWaqUCa1758ts/Jio84GZuzsvUkme98kv0WFY8//n0J1Z+vsIsQ==", + "dev": true, + "dependencies": { + "@octokit/request": "^8.3.0", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "22.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", + "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==", + "dev": true + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.3.1.tgz", + "integrity": "sha512-ryqobs26cLtM1kQxqeZui4v8FeznirUsksiA+RYemMPJ7Micju0WSkv50dBksTuZks9O5cg4wp+t8fZ/cLY56g==", + "dev": true, + "dependencies": { + "@octokit/types": "^13.5.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-4.0.1.tgz", + "integrity": "sha512-GihNqNpGHorUrO7Qa9JbAl0dbLnqJVrV8OXe2Zm5/Y4wFkZQDfTreBzVmiRfJVfE4mClXdihHnbpyyO9FSX4HA==", + "dev": true, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.2.2.tgz", + "integrity": "sha512-EI7kXWidkt3Xlok5uN43suK99VWqc8OaIMktY9d9+RNKl69juoTyxmLoWPIZgJYzi41qj/9zU7G/ljnNOJ5AFA==", + "dev": true, + "dependencies": { + "@octokit/types": "^13.5.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "^5" + } + }, + "node_modules/@octokit/request": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.0.tgz", + "integrity": "sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw==", + "dev": true, + "dependencies": { + "@octokit/endpoint": "^9.0.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.0.tgz", + "integrity": "sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q==", + "dev": true, + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest": { + "version": "20.1.1", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-20.1.1.tgz", + "integrity": "sha512-MB4AYDsM5jhIHro/dq4ix1iWTLGToIGk6cWF5L6vanFaMble5jTX/UBQyiv05HsWnwUtY8JrfHy2LWfKwihqMw==", + "dev": true, + "dependencies": { + "@octokit/core": "^5.0.2", + "@octokit/plugin-paginate-rest": "11.3.1", + "@octokit/plugin-request-log": "^4.0.0", + "@octokit/plugin-rest-endpoint-methods": "13.2.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/types": { + "version": "13.5.1", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.1.tgz", + "integrity": "sha512-F41lGiWBKPIWPBgjSvaDXTTQptBujnozENAK3S//nj7xsFdYdirImKlBB/hTjr+Vii68SM+8jG3UJWRa6DMuDA==", + "dev": true, + "dependencies": { + "@octokit/openapi-types": "^22.2.0" + } + }, "node_modules/@remix-run/router": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.2.tgz", @@ -1317,6 +1497,30 @@ "win32" ] }, + "node_modules/@tabler/icons": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.17.0.tgz", + "integrity": "sha512-sCSfAQ0w93KSnSL7tS08n73CdIKpuHP8foeLMWgDKiZaCs8ZE//N3ytazCk651ZtruTtByI3b+ZDj7nRf+hHvA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + } + }, + "node_modules/@tabler/icons-react": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.17.0.tgz", + "integrity": "sha512-Ndm9Htv7KpIU1PYYrzs5EMhyA3aZGcgaxUp9Q1XOxcRZ+I0X+Ub2WS5f4bkRyDdL1s0++k2T9XRgmg2pG113sw==", + "dependencies": { + "@tabler/icons": "3.17.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + }, + "peerDependencies": { + "react": ">= 16" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1376,6 +1580,15 @@ "devOptional": true, "license": "MIT" }, + "node_modules/@types/quill": { + "version": "1.3.10", + "resolved": "https://registry.npmjs.org/@types/quill/-/quill-1.3.10.tgz", + "integrity": "sha512-IhW3fPW+bkt9MLNlycw8u8fWb7oO7W5URC9MfZYHBlA24rex9rs23D5DETChu1zvgVdc5ka64ICjJOgQMr6Shw==", + "license": "MIT", + "dependencies": { + "parchment": "^1.1.2" + } + }, "node_modules/@types/react": { "version": "18.3.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.8.tgz", @@ -1729,6 +1942,23 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1736,6 +1966,12 @@ "dev": true, "license": "MIT" }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "dev": true + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1793,6 +2029,25 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1849,6 +2104,15 @@ "node": ">=4" } }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -1875,6 +2139,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1921,7 +2197,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true, "license": "MIT" }, "node_modules/debug": { @@ -1942,6 +2217,26 @@ } } }, + "node_modules/deep-equal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz", + "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==", + "license": "MIT", + "dependencies": { + "is-arguments": "^1.1.1", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.5.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1949,12 +2244,70 @@ "dev": true, "license": "MIT" }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "dev": true + }, "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.27", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.27.tgz", @@ -1962,6 +2315,27 @@ "dev": true, "license": "ISC" }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -2287,6 +2661,18 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", + "integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==", + "license": "MIT" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2294,6 +2680,12 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-diff": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz", + "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==", + "license": "Apache-2.0" + }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -2412,6 +2804,40 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2427,6 +2853,24 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2437,6 +2881,25 @@ "node": ">=6.9.0" } }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-nonce": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", @@ -2472,6 +2935,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -2489,6 +2964,69 @@ "node": ">=4" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2535,6 +3073,37 @@ "loose-envify": "^1.0.0" } }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2578,6 +3147,22 @@ "node": ">=8" } }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2691,6 +3276,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -2744,6 +3335,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2797,6 +3409,48 @@ "dev": true, "license": "MIT" }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -2847,6 +3501,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parchment": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz", + "integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==", + "license": "BSD-3-Clause" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -3053,6 +3713,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3084,6 +3760,34 @@ ], "license": "MIT" }, + "node_modules/quill": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz", + "integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==", + "license": "BSD-3-Clause", + "dependencies": { + "clone": "^2.1.1", + "deep-equal": "^1.0.1", + "eventemitter3": "^2.0.3", + "extend": "^3.0.2", + "parchment": "^1.1.4", + "quill-delta": "^3.6.2" + } + }, + "node_modules/quill-delta": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz", + "integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==", + "license": "MIT", + "dependencies": { + "deep-equal": "^1.0.1", + "extend": "^3.0.2", + "fast-diff": "1.1.2" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -3109,6 +3813,11 @@ "react": "^18.3.1" } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/react-number-format": { "version": "5.4.2", "resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.4.2.tgz", @@ -3119,6 +3828,21 @@ "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-quill": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/react-quill/-/react-quill-2.0.0.tgz", + "integrity": "sha512-4qQtv1FtCfLgoD3PXAur5RyxuUbPXQGOHgTlFie3jtxp43mXDtzCKaOgQ3mLyZfi1PUlyjycfivKelFhy13QUg==", + "license": "MIT", + "dependencies": { + "@types/quill": "^1.3.10", + "lodash": "^4.17.4", + "quill": "^1.3.7" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "react-dom": "^16 || ^17 || ^18" + } + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -3248,12 +3972,45 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "license": "MIT" }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3354,6 +4111,38 @@ "semver": "bin/semver.js" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3561,6 +4350,12 @@ } } }, + "node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "dev": true + }, "node_modules/update-browserslist-db": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", @@ -3752,6 +4547,24 @@ } } }, + "node_modules/vite-plugin-mkcert": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/vite-plugin-mkcert/-/vite-plugin-mkcert-1.17.6.tgz", + "integrity": "sha512-4JR1RN0HEg/w17eRQJ/Ve2pSa6KCVQcQO6yKtIaKQCFDyd63zGfXHWpygBkvvRSpqa0GcqNKf0fjUJ0HiJQXVQ==", + "dev": true, + "dependencies": { + "@octokit/rest": "^20.1.1", + "axios": "^1.7.4", + "debug": "^4.3.6", + "picocolors": "^1.0.1" + }, + "engines": { + "node": ">=v16.7.0" + }, + "peerDependencies": { + "vite": ">=3" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3778,6 +4591,12 @@ "node": ">=0.10.0" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/peer-prep/package.json b/peer-prep/package.json index 6c57046897..504932e590 100644 --- a/peer-prep/package.json +++ b/peer-prep/package.json @@ -12,8 +12,11 @@ "dependencies": { "@mantine/core": "^7.12.2", "@mantine/hooks": "^7.12.2", + "@mantine/notifications": "^7.13.0", + "@tabler/icons-react": "^3.17.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-quill": "^2.0.0", "react-router-dom": "^6.26.2" }, "devDependencies": { @@ -30,6 +33,7 @@ "postcss-simple-vars": "^7.0.1", "typescript": "^5.5.3", "typescript-eslint": "^8.0.1", - "vite": "^5.4.1" + "vite": "^5.4.1", + "vite-plugin-mkcert": "^1.17.6" } } diff --git a/peer-prep/postcss.config.cjs b/peer-prep/postcss.config.cjs index ba1531a83f..bfba0ddfae 100644 --- a/peer-prep/postcss.config.cjs +++ b/peer-prep/postcss.config.cjs @@ -1,14 +1,14 @@ module.exports = { - plugins: { - 'postcss-preset-mantine': {}, - 'postcss-simple-vars': { - variables: { - 'mantine-breakpoint-xs': '36em', - 'mantine-breakpoint-sm': '48em', - 'mantine-breakpoint-md': '62em', - 'mantine-breakpoint-lg': '75em', - 'mantine-breakpoint-xl': '88em', - }, - }, + plugins: { + 'postcss-preset-mantine': {}, + 'postcss-simple-vars': { + variables: { + 'mantine-breakpoint-xs': '36em', + 'mantine-breakpoint-sm': '48em', + 'mantine-breakpoint-md': '62em', + 'mantine-breakpoint-lg': '75em', + 'mantine-breakpoint-xl': '88em', + }, }, + }, }; diff --git a/peer-prep/src/App.tsx b/peer-prep/src/App.tsx index 82b54e3e96..4bd75c5904 100644 --- a/peer-prep/src/App.tsx +++ b/peer-prep/src/App.tsx @@ -1,20 +1,23 @@ -import '@mantine/core/styles.css'; -import { MantineProvider } from '@mantine/core'; -import { Routes, Route } from 'react-router-dom'; -import Home from './pages/Home'; -import Login from './pages/Login'; +// import "@mantine/core/styles.css"; +// import { MantineProvider } from "@mantine/core"; +// import { Routes, Route } from "react-router-dom"; +// import Home from "./pages/Home"; +// import Login from "./pages/Login"; -function App() { +// function App() { +// return ( +// +// {" "} +// { +//
+// +// }> +// }> +// +//
+// } +//
+// ); +// } - return { -
-
Navbar
- - }> - }> - -
- }
-} - -export default App +// export default App; diff --git a/peer-prep/src/assets/loginimage.svg b/peer-prep/src/assets/loginimage.svg new file mode 100644 index 0000000000..a99d170c3f --- /dev/null +++ b/peer-prep/src/assets/loginimage.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/peer-prep/src/assets/searchimage.svg b/peer-prep/src/assets/searchimage.svg new file mode 100644 index 0000000000..d7f4e6d0ce --- /dev/null +++ b/peer-prep/src/assets/searchimage.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/peer-prep/src/components/ApplicationWrapper.tsx b/peer-prep/src/components/ApplicationWrapper.tsx new file mode 100644 index 0000000000..d934e5b9ab --- /dev/null +++ b/peer-prep/src/components/ApplicationWrapper.tsx @@ -0,0 +1,15 @@ +import { Outlet } from "react-router-dom"; +import { Navbar } from "./Navbar/Navbar"; +import { AuthProvider } from "../hooks/useAuth"; + +// https://stackoverflow.com/questions/70833727/using-react-router-v6-i-need-a-navbar-to-permanently-be-there-but-cant-display +export default function ApplicationWrapper() { + return ( +
+ + + + +
+ ); +} diff --git a/peer-prep/src/components/AvatarIcon/AvatarWithDetailsButton.tsx b/peer-prep/src/components/AvatarIcon/AvatarWithDetailsButton.tsx new file mode 100644 index 0000000000..c6ebf855d5 --- /dev/null +++ b/peer-prep/src/components/AvatarIcon/AvatarWithDetailsButton.tsx @@ -0,0 +1,42 @@ +import { forwardRef } from "react"; +import { IconChevronRight } from "@tabler/icons-react"; +import { Group, Avatar, Text, Menu, UnstyledButton } from "@mantine/core"; + +interface UserButtonProps extends React.ComponentPropsWithoutRef<"button"> { + image: string; + name: string; + email: string; + icon?: React.ReactNode; +} + +const AvatarWithDetailsButton = forwardRef( + ({ image, name, email, icon, ...others }: UserButtonProps, ref) => ( + + + + +
+ + {name} + + + + {email} + +
+ + {icon || } +
+
+ ) +); + +export default AvatarWithDetailsButton; diff --git a/peer-prep/src/components/Navbar/Navbar.module.css b/peer-prep/src/components/Navbar/Navbar.module.css new file mode 100644 index 0000000000..de85cf8bd8 --- /dev/null +++ b/peer-prep/src/components/Navbar/Navbar.module.css @@ -0,0 +1,62 @@ +.icon { + font-size: larger; + font-weight: 700; +} + +.header { + height: rem(60px); + padding-left: var(--mantine-spacing-md); + padding-right: var(--mantine-spacing-md); + border-bottom: rem(1px) solid + light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)); +} + +.link { + display: flex; + align-items: center; + height: 100%; + padding-left: var(--mantine-spacing-md); + padding-right: var(--mantine-spacing-md); + text-decoration: none; + color: light-dark(var(--mantine-color-black), var(--mantine-color-white)); + font-weight: 500; + font-size: var(--mantine-font-size-sm); + + @media (max-width: $mantine-breakpoint-sm) { + height: rem(42px); + width: 100%; + } + + @mixin hover { + background-color: light-dark( + var(--mantine-color-gray-0), + var(--mantine-color-dark-6) + ); + } +} + +.subLink { + width: 100%; + padding: var(--mantine-spacing-xs) var(--mantine-spacing-md); + border-radius: var(--mantine-radius-md); + + @mixin hover { + background-color: light-dark( + var(--mantine-color-gray-0), + var(--mantine-color-dark-7) + ); + } +} + +.dropdownFooter { + background-color: light-dark( + var(--mantine-color-gray-0), + var(--mantine-color-dark-7) + ); + margin: calc(var(--mantine-spacing-md) * -1); + margin-top: var(--mantine-spacing-sm); + padding: var(--mantine-spacing-md) calc(var(--mantine-spacing-md) * 2); + padding-bottom: var(--mantine-spacing-xl); + border-top: rem(1px) solid + light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5)); +} diff --git a/peer-prep/src/components/Navbar/Navbar.tsx b/peer-prep/src/components/Navbar/Navbar.tsx new file mode 100644 index 0000000000..b5c87a388a --- /dev/null +++ b/peer-prep/src/components/Navbar/Navbar.tsx @@ -0,0 +1,339 @@ +import { + HoverCard, + Group, + Button, + UnstyledButton, + Text, + SimpleGrid, + ThemeIcon, + Anchor, + Divider, + Center, + Box, + Burger, + Drawer, + Collapse, + ScrollArea, + rem, + useMantineTheme, + Menu, +} from "@mantine/core"; +// import { MantineLogo } from "@mantinex/mantine-logo"; +import { useDisclosure } from "@mantine/hooks"; +import { + IconNotification, + IconCode, + IconBook, + IconChartPie3, + IconFingerprint, + IconCoin, + IconChevronDown, + IconArrowsLeftRight, + IconMessageCircle, + IconPhoto, + IconSearch, + IconSettings, + IconTrash, + IconLogout, + IconHome, +} from "@tabler/icons-react"; +import classes from "./Navbar.module.css"; +import { Link } from "react-router-dom"; +import { useAuth } from "../../hooks/useAuth"; +import AvatarWithDetailsButton from "../AvatarIcon/AvatarWithDetailsButton"; + +const mockdata = [ + { + icon: IconCode, + title: "Open source", + description: "This Pokémon’s cry is very loud and distracting", + }, + { + icon: IconCoin, + title: "Free for everyone", + description: "The fluid of Smeargle’s tail secretions changes", + }, + { + icon: IconBook, + title: "Documentation", + description: "Yanma is capable of seeing 360 degrees without", + }, + { + icon: IconFingerprint, + title: "Security", + description: "The shell’s rounded shape and the grooves on its.", + }, + { + icon: IconChartPie3, + title: "Analytics", + description: "This Pokémon uses its flying ability to quickly chase", + }, + { + icon: IconNotification, + title: "Notifications", + description: "Combusken battles with the intensely hot flames it spews", + }, +]; + +export function Navbar() { + const [drawerOpened, { toggle: toggleDrawer, close: closeDrawer }] = + useDisclosure(false); + const [linksOpened, { toggle: toggleLinks }] = useDisclosure(false); + const theme = useMantineTheme(); + + const links = mockdata.map((item) => ( + + + + + +
+ + {item.title} + + + {item.description} + +
+
+
+ )); + + const { user, logout } = useAuth(); + + return ( + +
+ + {/* */} + 🫂 PeerPrep + + + + Home + + + {/* + + +
+ + Features + + +
+
+
+ + + + Features + + View all + + + + + + + {links} + + +
+ +
+ + Get started + + + Their food sources have decreased, and their numbers + +
+ +
+
+
+
*/} + + Learn + + + + Academy + + + Questions + +
+ + {user ? ( + + + + + + + Application + + } + > + + Dashboard + + + + + } + > + Messages + + {/* + } + > + Gallery + */} + + + } + rightSection={ + + ⌘K + + } + > + Search + + + + + + } + > + Settings + + + {/* Danger zone + + } + > + Transfer my data + */} + {/* + } + > + Delete my account + */} + + + } + onClick={logout} + > + Logout + + + + ) : ( + + + + + + + + + )} + + +
+
+ + + + + + + Home + + {/* +
+ + Features + + +
+
*/} + {links} + + Learn + + + + Academy + + + + + + + + + + +
+
+
+ ); +} diff --git a/peer-prep/src/components/Questions/Category/Category.module.css b/peer-prep/src/components/Questions/Category/Category.module.css new file mode 100644 index 0000000000..b3e0c0125a --- /dev/null +++ b/peer-prep/src/components/Questions/Category/Category.module.css @@ -0,0 +1,13 @@ +.category { + background-color: light-dark(var(--mantine-color-blue-2), "blue"); + padding: 0.25rem 0.5rem; + display: flex; + border-radius: var(--mantine-radius-sm); + align-items: center; + justify-content: center; +} + +.category-text { + font-size: var(--mantine-font-size-xs); + text-transform: uppercase; +} diff --git a/peer-prep/src/components/Questions/Category/Category.tsx b/peer-prep/src/components/Questions/Category/Category.tsx new file mode 100644 index 0000000000..46767d9d86 --- /dev/null +++ b/peer-prep/src/components/Questions/Category/Category.tsx @@ -0,0 +1,14 @@ +import { Box, Text } from "@mantine/core"; + +import classes from "./Category.module.css"; + +export default function CategoryDisplay({ category }: { category: string }) { + // based on the category prop, choose a color based on the first two letters + // of the category + + return ( + + {category} + + ); +} diff --git a/peer-prep/src/components/Questions/Complexity/Complexity.tsx b/peer-prep/src/components/Questions/Complexity/Complexity.tsx new file mode 100644 index 0000000000..2cfae6d94d --- /dev/null +++ b/peer-prep/src/components/Questions/Complexity/Complexity.tsx @@ -0,0 +1,19 @@ +import { Rating } from "@mantine/core"; +import { Complexity } from "../../../types/question"; + +export default function ComplexityDisplay({ + complexity, +}: { + complexity: Complexity; +}) { + let rating = 0; + if (complexity == "EASY") { + rating = 1; + } else if (complexity == "MEDIUM") { + rating = 2; + } else if (complexity == "HARD") { + rating = 3; + } + + return ; +} diff --git a/peer-prep/src/components/Questions/QuestionCard/QuestionCard.module.css b/peer-prep/src/components/Questions/QuestionCard/QuestionCard.module.css new file mode 100644 index 0000000000..74e740f32e --- /dev/null +++ b/peer-prep/src/components/Questions/QuestionCard/QuestionCard.module.css @@ -0,0 +1,32 @@ +.card { + max-width: 320px; + padding: 1rem; + border-radius: var(--mantine-radius-md); + border: rem(1px) solid + light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-7)); + word-wrap: break-word; + overflow-wrap: break-word; + white-space: normal; +} + +.contents { + height: 300px; + width: 280px; +} + +.title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + height: 60px; + max-width: 100%; +} + +.description { + white-space: normal; + overflow-wrap: break-word; + height: 180px; + max-width: 100%; + white-space: "pre-wrap"; + flex: 1, +} diff --git a/peer-prep/src/components/Questions/QuestionCard/QuestionCard.tsx b/peer-prep/src/components/Questions/QuestionCard/QuestionCard.tsx new file mode 100644 index 0000000000..2cc8e91d1c --- /dev/null +++ b/peer-prep/src/components/Questions/QuestionCard/QuestionCard.tsx @@ -0,0 +1,68 @@ +import { + Badge, + Box, + Button, + Divider, + Flex, + Group, + rem, + Stack, + Text, + Title, +} from "@mantine/core"; +import { QuestionOlsd } from "../../../types/question"; + +import classes from "./QuestionCard.module.css"; +import ComplexityDisplay from "../Complexity/Complexity"; +import { IconBrandLeetcode } from "@tabler/icons-react"; +import { Link, useNavigate } from "react-router-dom"; +import CategoryDisplay from "../Category/Category"; + +interface QuestionCardProps { + question: QuestionOlsd | any; + isClickable?: boolean; // New prop to control click behavior +} + +export default function QuestionCard({ + question, + isClickable = false, // Default to false +}: QuestionCardProps) { + const navigate = useNavigate(); + + const handleCardClick = () => { + if (isClickable) { + navigate(`/questions/edit/${question._id}`); + } + } + + return ( + + + + {question.title} + + + {question.description.testDescription } + + + {question.categories.map((category, index) => ( + + ))} + + + + + + + + + ); +} diff --git a/peer-prep/src/hooks/useApi.tsx b/peer-prep/src/hooks/useApi.tsx new file mode 100644 index 0000000000..e28dc207f9 --- /dev/null +++ b/peer-prep/src/hooks/useApi.tsx @@ -0,0 +1,100 @@ +import { useState } from "react"; +import { User } from "../types/user"; +import { useLocalStorage } from "@mantine/hooks"; +import { useAuth } from "./useAuth"; +import { useNavigate } from "react-router-dom"; + +export interface QuestionServerResponse { + success: boolean; + status: number; + data: T; + message?: string; +} + +export interface UserServerResponse { + user?: T; + message: string; +} + +// export interface ServerError { +// message: string; +// } + +export enum SERVICE { + USER, + QUESTION, +} + +export default function useApi() { + // const [data, setData] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const navigate = useNavigate(); + + const [user, setUser] = useLocalStorage({ + key: "user", + defaultValue: null, + }); + + async function fetchData( + url: string, + service: SERVICE, + options?: RequestInit, + suppressWarning?: boolean + ) { + setIsLoading(true); + try { + // todo make this nicer... + const response = await fetch( + `${ + service === SERVICE.USER + ? import.meta.env.VITE_API_URL_USER + : import.meta.env.VITE_API_URL_QUESTION + }${url}`, + { + ...options, + headers: { + ...options?.headers, + // "x-api-key": import.meta.env.VITE_API_KEY as string, + // bearer token + // "Authorization": `Bearer ${accessToken}`, + }, + credentials: "include", + } + ); + + // if the response status indicates not logged in, clear data and redirect to login + // if (response.status === 401) { + // // + // // + // return; + // } + + if (response.status === 401) { + setUser(null); + navigate("/login"); + } + + const data: Q = await response.json(); + + if (!response.ok) { + console.log("RESPONSE NOT OK!"); + // @ts-ignore + throw data; + } + + // setData(data); + return data; + } catch (error) { + setError(error); + + // throw error again + throw error; + } finally { + setIsLoading(false); + } + } + + return { isLoading, error, fetchData }; +} diff --git a/peer-prep/src/hooks/useAuth.tsx b/peer-prep/src/hooks/useAuth.tsx new file mode 100644 index 0000000000..b5322b55cc --- /dev/null +++ b/peer-prep/src/hooks/useAuth.tsx @@ -0,0 +1,170 @@ +import { useLocalStorage } from "@mantine/hooks"; +import { createContext, ReactElement, useContext, useMemo } from "react"; +import { useNavigate } from "react-router-dom"; +import { User } from "../types/user"; +import useApi, { SERVICE, UserServerResponse } from "./useApi"; +import { notifications } from "@mantine/notifications"; + +export interface AuthContextType { + user: User | null; + login: (data: any) => void; + logout: () => void; + register: (data: any) => void; +} + +const DEFAULT_TEMP_USER: User = { + email: "johndoe@gmail.com", + displayName: "John Doe", + isAdmin: false, + id: "123", + // password: "password", +}; + +const DEFAULT: AuthContextType = { + user: DEFAULT_TEMP_USER, // TODO: switch this out later + login: () => { + console.log("log"); + }, + logout: () => {}, + register: () => {}, +}; +const AuthContext = createContext(DEFAULT); + +export const AuthProvider = ({ + children, +}: { + children: ReactElement | ReactElement[]; +}) => { + const [user, setUser] = useLocalStorage({ + key: "user", + defaultValue: null, + }); + + const { fetchData, isLoading, error } = useApi(); + const navigate = useNavigate(); + + // call this function when you want to authenticate the user + const login = async (data: { email: string; password: string }) => { + fetchData>( + `/user-service/users/login`, + SERVICE.USER, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + } + ) + .then((data) => { + // ok! + setUser(data.user || null); + console.log(data.user, "<<<<"); + navigate("/dashboard", { replace: true }); + + notifications.show({ + message: "Welcome to PeerPrep!", + title: "Login successful", + color: "green", + }); + }) + .catch((e) => { + console.log("ERROR:: Login failed", e); + + notifications.show({ + message: e.message || "Login failed", + title: "Error - Login failed!", + color: "red", + }); + }); + }; + + const register = async (data: { + email: string; + password: string; + displayName: string; + }) => { + fetchData(`/user-service/users`, SERVICE.USER, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }) + .then((_) => { + // ok! + notifications.show({ + message: "Registration successful", + color: "green", + }); + + // need to login to set cookie + login({ + email: data.email, + password: data.password, + }); + return; + }) + .catch((e) => { + console.error("ERROR:: Registration failed"); + + notifications.show({ + message: e.message || "Registration failed", + title: "Error - Registration failed!", + color: "red", + }); + }); + // const response = await fetch(`/user-service/users`); + + // if (!response.ok) { + // console.error("ERROR:: Registration failed"); + // return; + // } + + // if (response.status === 201) { + // console.log("INFO:: Registration successful"); + + // // login the user + // } + }; + + // call this function to sign out logged in user + const logout = () => { + fetchData(`/user-service/users/logout`, SERVICE.USER, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({}), + }) + .then((_) => { + // ok! + setUser(null); + navigate("/", { replace: true }); + + notifications.show({ + message: "Logged out", + title: "Success", + color: "green", + }); + }) + .catch((e) => { + console.error("ERROR:: Logout failed"); + }); + }; + + const value = useMemo( + () => ({ + user, + login, + logout, + register, + }), + [user] + ); + return {children}; +}; + +export const useAuth = () => { + return useContext(AuthContext); +}; diff --git a/peer-prep/src/index.css b/peer-prep/src/index.css index e69de29bb2..b59b020c4f 100644 --- a/peer-prep/src/index.css +++ b/peer-prep/src/index.css @@ -0,0 +1,12 @@ +/* always scrollbar */ +html { + overflow-y: scroll; +} + +.ql-toolbar.ql-snow { + border-top: none !important; + border-left: none !important; + border-right: none !important; + /* border-bottom: 1px solid var(--mantine-color-gray-2) !important; */ + border-bottom: none !important; +} diff --git a/peer-prep/src/main.tsx b/peer-prep/src/main.tsx index f97de2eb89..739abc18b6 100644 --- a/peer-prep/src/main.tsx +++ b/peer-prep/src/main.tsx @@ -1,13 +1,133 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import App from './App.tsx' -import './index.css' -import { BrowserRouter } from 'react-router-dom' +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +// import App from "./App.tsx"; +import "./index.css"; +import { + BrowserRouter, + createBrowserRouter, + RouterProvider, +} from "react-router-dom"; +import Home from "./pages/Home/HomePage.tsx"; +import LoginOrRegisterPage from "./pages/Login/LoginPage.tsx"; +import { Button, createTheme, MantineProvider } from "@mantine/core"; +import "@mantine/core/styles.css"; +import "@mantine/notifications/styles.css"; -createRoot(document.getElementById('root')!).render( +import ApplicationWrapper from "./components/ApplicationWrapper.tsx"; +import ProtectedRouteWrapper from "./pages/ProtectedRouteWrapper.tsx"; +import { AuthProvider } from "./hooks/useAuth.tsx"; +import DashboardPage from "./pages/Dashboard/DashboardPage.tsx"; +import SearchingPage from "./pages/Session/Search/SearchingPage.tsx"; +import CreateSessionPage from "./pages/Session/Create/CreateSessionPage.tsx"; +import QuestionPage from "./pages/Questions/QuestionPage.tsx"; +import CreateQuestionPage from "./pages/Questions/CreateQuestionPage/CreateQuestionPage.tsx"; +import EditQuestionPage from "./pages/Questions/EditQuestionPage/EditQuestionPage.tsx"; +import { Notifications } from "@mantine/notifications"; +import AdminRouteWrapper from "./pages/AdminRouteWrapper.tsx"; +const router = createBrowserRouter([ + { + path: "/", + element: , + children: [ + { + path: "/", + element: , + }, + { + path: "/login", + element: , + }, + + // Protected routes below + { + path: "/", + element: , + children: [ + // admin-only routes + { + path: "/", + element: , + children: [ + { + path: "/questions", + children: [ + { + path: "/questions", + element: , + loader: async () => + fetch( + `${ + import.meta.env.VITE_API_URL_QUESTION + }/question-service` + ), + // fetch( + // `https://virtserver.swaggerhub.com/PeerPrep/question-service/1.0.0/api/question-service` + // ), + }, + { + path: "/questions/create", + element: , + }, + { + path: "/questions/edit/:id", + element: , + }, + ], + }, + ], + }, + + { + path: "/dashboard", + element: , + }, + { + path: "/learn", + element: , + }, + + { + path: "/session", + children: [ + { + path: "/session/create", + element: , + // loader: async () => { + // return await Promise.all([ + // fetch( + // `${ + // import.meta.env.VITE_API_URL + // }/question-service/categories` + // ), + // ]); + // }, + }, + { + path: "/session/search", + element: , + }, + ], + }, + ], + }, + ], + }, +]); + +const theme = createTheme({ + fontFamily: "Montserrat, sans-serif", + defaultRadius: "md", + cursorType: "pointer", +}); + +createRoot(document.getElementById("root")!).render( - + + + + + {/* - - , -) + */} + +); diff --git a/peer-prep/src/pages/AdminRouteWrapper.tsx b/peer-prep/src/pages/AdminRouteWrapper.tsx new file mode 100644 index 0000000000..8efd22e8bf --- /dev/null +++ b/peer-prep/src/pages/AdminRouteWrapper.tsx @@ -0,0 +1,28 @@ +import { Navigate, Outlet, useNavigate } from "react-router-dom"; +import { useAuth } from "../hooks/useAuth"; +import { notifications } from "@mantine/notifications"; + +// https://stackoverflow.com/questions/70833727/using-react-router-v6-i-need-a-navbar-to-permanently-be-there-but-cant-display +export default function AdminRouteWrapper() { + const { user } = useAuth(); + const navigate = useNavigate(); + if (!user || !user.isAdmin) { + // user is not admin + // show an error admin message + notifications.show({ + message: "You are not authorized to view this page!", + title: "Authorization error!", + color: "red", + }); + + // navigate to previous page + // navigate(-1); + // return; + return ; + } + return ( +
+ +
+ ); +} diff --git a/peer-prep/src/pages/Dashboard/DashboardPage.module.css b/peer-prep/src/pages/Dashboard/DashboardPage.module.css new file mode 100644 index 0000000000..63a6c4e2cd --- /dev/null +++ b/peer-prep/src/pages/Dashboard/DashboardPage.module.css @@ -0,0 +1,7 @@ +.question-list { + display: flex; + /* flex-direction: column; */ + flex-wrap: wrap; + gap: 1rem; + margin-top: 1rem; +} diff --git a/peer-prep/src/pages/Dashboard/DashboardPage.tsx b/peer-prep/src/pages/Dashboard/DashboardPage.tsx new file mode 100644 index 0000000000..9359d94fab --- /dev/null +++ b/peer-prep/src/pages/Dashboard/DashboardPage.tsx @@ -0,0 +1,54 @@ +import { + Avatar, + Box, + Button, + Center, + Container, + Flex, + Group, + SimpleGrid, + Stack, + Text, + Title, +} from "@mantine/core"; +import { useAuth } from "../../hooks/useAuth"; +import QuestionCard from "../../components/Questions/QuestionCard/QuestionCard"; +import { SAMPLE_QUESTIONS } from "../../types/question"; + +import classes from "./DashboardPage.module.css"; +import { Link } from "react-router-dom"; +export default function DashboardPage() { + const { user } = useAuth(); + + return ( + <> +
+ + + + + {user?.displayName} + {user?.email} + +
+ +
+
+
+
+
+ {/* */} + + + {SAMPLE_QUESTIONS.map((question, key) => ( + + ))} + + + {/* */} +
+ + ); +} diff --git a/peer-prep/src/pages/Home.tsx b/peer-prep/src/pages/Home.tsx deleted file mode 100644 index bcbbe36098..0000000000 --- a/peer-prep/src/pages/Home.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function Home() { - return ( -
-

Peer-Prep Home Page

-
- ); -} \ No newline at end of file diff --git a/peer-prep/src/pages/Home/HomePage.module.css b/peer-prep/src/pages/Home/HomePage.module.css new file mode 100644 index 0000000000..9887a97f4a --- /dev/null +++ b/peer-prep/src/pages/Home/HomePage.module.css @@ -0,0 +1,25 @@ +.header { + min-height: calc(100vh - 60px - var(--mantine-spacing-md)); + /* background-color: var(--mantine-color-gray-0); */ +} + +.header-content { + min-height: 100vh; +} +.title { + text-align: center; + font-size: 24rem; +} + +.description { + text-align: center; +} + +.wrapper { + padding-top: calc(var(--mantine-spacing-xl) * 4); + padding-bottom: calc(var(--mantine-spacing-xl) * 4); +} + +.shaded { + background-color: var(--mantine-color-gray-0); +} diff --git a/peer-prep/src/pages/Home/HomePage.tsx b/peer-prep/src/pages/Home/HomePage.tsx new file mode 100644 index 0000000000..437f01acf1 --- /dev/null +++ b/peer-prep/src/pages/Home/HomePage.tsx @@ -0,0 +1,165 @@ +import { + Box, + Button, + Center, + Container, + Group, + rem, + SimpleGrid, + Stack, + Text, + ThemeIcon, + Title, +} from "@mantine/core"; +import { + IconGauge, + IconCookie, + IconUser, + IconMessage2, + IconLock, +} from "@tabler/icons-react"; +import classes from "./HomePage.module.css"; + +export const MOCKDATA = [ + { + icon: IconGauge, + title: "Extreme performance", + description: + "This dust is actually a powerful poison that will even make a pro wrestler sick, Regice cloaks itself with frigid air of -328 degrees Fahrenheit", + }, + { + icon: IconUser, + title: "Privacy focused", + description: + "People say it can run at the same speed as lightning striking, Its icy body is so cold, it will not melt even if it is immersed in magma", + }, + { + icon: IconCookie, + title: "No third parties", + description: + "They’re popular, but they’re rare. Trainers who show them off recklessly may be targeted by thieves", + }, + { + icon: IconLock, + title: "Secure by default", + description: + "Although it still can’t fly, its jumping power is outstanding, in Alola the mushrooms on Paras don’t grow up quite right", + }, + { + icon: IconMessage2, + title: "24/7 Support", + description: + "Rapidash usually can be seen casually cantering in the fields and plains, Skitty is known to chase around after its own tail", + }, +]; + +interface FeatureProps { + icon: React.FC; + title: React.ReactNode; + description: React.ReactNode; +} + +export default function Home() { + return ( + <> +
+ +
+ + + Welcome to PeerPrep! + + + {" "} + At PeerPrep, we want you to ace your coding interviews. But, + doing it alone is boring. Find someone to collaborate with on a + coding problem, with test cases, timed submission, and more!{" "} + + +
+ +
+
+
+
+
+
+ + + + Integrate effortlessly with any technology stack + + + + + Every once in a while, you’ll see a Golbat that’s missing some + fangs. This happens when hunger drives it to try biting a + Steel-type Pokémon. + + + + + {MOCKDATA.map(Feature)} + + + +
+ + ); +} + +export function Feature( + { icon: Icon, title, description }: FeatureProps, + index: number +) { + return ( +
+ + + + + {title} + + + {description} + +
+ ); +} + +export function FeaturesGrid() { + const features = MOCKDATA.map((feature, index) => ( + + )); + + return ( + + + Integrate effortlessly with any technology stack + + + + + Every once in a while, you’ll see a Golbat that’s missing some fangs. + This happens when hunger drives it to try biting a Steel-type Pokémon. + + + + + {features} + + + ); +} diff --git a/peer-prep/src/pages/Login.tsx b/peer-prep/src/pages/Login.tsx deleted file mode 100644 index 8519d930f6..0000000000 --- a/peer-prep/src/pages/Login.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function Login() { - return ( -
-

Login to Peer-Prep

-
- ); -} \ No newline at end of file diff --git a/peer-prep/src/pages/Login/LoginPage.module.css b/peer-prep/src/pages/Login/LoginPage.module.css new file mode 100644 index 0000000000..31cb50b7b2 --- /dev/null +++ b/peer-prep/src/pages/Login/LoginPage.module.css @@ -0,0 +1,40 @@ +.wrapper { + /* min-height: rem(900px); + background-size: cover; + background-image: url(https://images.unsplash.com/photo-1484242857719-4b9144542727?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1280&q=80); */ +} + +.form { + border-left: rem(1px) solid + light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-7)); + /* min-height: rem(900px); */ + min-height: calc(100vh - 60px - var(--mantine-spacing-md)); + /* max-width: rem(450px); */ + padding: 3rem; + padding-right: 4rem; + padding-left: 4rem; + + justify-content: center; + flex-direction: column; + + @media (max-width: $mantine-breakpoint-sm) { + max-width: 100%; + flex-grow: 1; + } +} + +.left-image-container { + max-width: 1600px; + + @media (max-width: $mantine-breakpoint-md) { + display: none; + } +} +/* .left-image { + max-width: 600px; +} */ + +.title { + color: light-dark(var(--mantine-color-black), var(--mantine-color-white)); + font-family: Greycliff CF, var(--mantine-font-family); +} diff --git a/peer-prep/src/pages/Login/LoginPage.tsx b/peer-prep/src/pages/Login/LoginPage.tsx new file mode 100644 index 0000000000..a8e13b4256 --- /dev/null +++ b/peer-prep/src/pages/Login/LoginPage.tsx @@ -0,0 +1,279 @@ +import { + Paper, + TextInput, + PasswordInput, + Checkbox, + Button, + Title, + Text, + Anchor, + Box, + Flex, + Center, + Stack, + Image, + Progress, + Group, +} from "@mantine/core"; +import classes from "./LoginPage.module.css"; +import image from "../../assets/loginimage.svg"; +import { useEffect, useState } from "react"; +import { useAuth } from "../../hooks/useAuth"; +import { Navigate, useLocation } from "react-router-dom"; +import { IconCheck, IconX } from "@tabler/icons-react"; + +function PasswordRequirement({ + meets, + label, +}: { + meets: boolean; + label: string; +}) { + return ( + +
+ {meets ? ( + + ) : ( + + )} + {label} +
+
+ ); +} + +const requirements = [ + { re: /[0-9]/, label: "Includes number" }, + { re: /[a-zA-Z]/, label: "Includes alphabets" }, + { label: "At least 8 characters", re: /.{8,}/ }, +]; + +function getStrength(password: string) { + let multiplier = password.length > 5 ? 0 : 1; + + requirements.forEach((requirement) => { + if (!requirement.re.test(password)) { + multiplier += 1; + } + }); + + return Math.max(100 - (100 / (requirements.length + 1)) * multiplier, 0); +} + +export default function LoginOrRegisterPage() { + const [loginMode, setLoginMode] = useState(true); // true = log in, false = register + + const { login, user, register } = useAuth(); + + if (user) { + return ; + } + + // if register mode from query params, show register form + const { search } = useLocation(); + const searchParams = new URLSearchParams(search); + const param = searchParams.get("register"); + useEffect(() => { + if (param) { + setLoginMode(false); + } else { + setLoginMode(true); + } + }, [param]); + + const [displayName, setDisplayName] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + + const [isLoading, setIsLoading] = useState(false); + + const handleLogin = () => { + // validate + + // call + login({ + email, + password, + }); + }; + + const handleRegister = () => { + // validate + + // call + setIsLoading(true); + console.log({ email, password, displayName }); + register({ + email, + password, + displayName, + }); + }; + + const strength = getStrength(password); + const checks = requirements.map((requirement, index) => ( + + )); + + const canRegister = requirements.every((requirement) => + requirement.re.test(password) + ); + const bars = Array(4) + .fill(0) + .map((_, index) => ( + 0 && index === 0 + ? 100 + : strength >= ((index + 1) / 4) * 100 + ? 100 + : 0 + } + color={strength > 80 ? "teal" : strength > 50 ? "yellow" : "red"} + key={index} + size={4} + /> + )); + + function onSubmit() { + if (loginMode) { + handleLogin(); + } else { + handleRegister(); + } + } + + return ( + //
+
{ + e.preventDefault(); + onSubmit(); + }} + > + + + + + + + Welcome {loginMode && "back"} to PeerPrep! + + + {!loginMode && ( + setDisplayName(event.currentTarget.value)} + value={displayName} + /> + )} + + setEmail(event.currentTarget.value)} + value={email} + // email must include an "@" + pattern="[^@\s]+@[^@\s]+\.[^@\s]+" + /> + setPassword(event.currentTarget.value)} + value={password} + /> + {!loginMode && ( + + {bars} + + )} + + {/* {!loginMode && ( + 5} + /> + )} */} + {!loginMode && checks} + + {loginMode ? ( + + ) : ( + + )} + + {loginMode ? ( + + Don't have an account?{" "} + + href="#" + fw={700} + onClick={(event) => { + event.preventDefault(); + setLoginMode(false); + }} + > + Register + + + ) : ( + + Already have an account?{" "} + + href="#" + fw={700} + onClick={(event) => { + event.preventDefault(); + setLoginMode(true); + }} + > + Login instead + + + )} + + +
+ //
+ ); +} diff --git a/peer-prep/src/pages/ProtectedRouteWrapper.tsx b/peer-prep/src/pages/ProtectedRouteWrapper.tsx new file mode 100644 index 0000000000..01bb77eafa --- /dev/null +++ b/peer-prep/src/pages/ProtectedRouteWrapper.tsx @@ -0,0 +1,16 @@ +import { Navigate, Outlet } from "react-router-dom"; +import { useAuth } from "../hooks/useAuth"; + +// https://stackoverflow.com/questions/70833727/using-react-router-v6-i-need-a-navbar-to-permanently-be-there-but-cant-display +export default function ProtectedRouteWrapper() { + const { user } = useAuth(); + if (!user) { + // user is not authenticated + return ; + } + return ( +
+ +
+ ); +} diff --git a/peer-prep/src/pages/Questions/CreateQuestionPage/CreateQuestionPage.module.css b/peer-prep/src/pages/Questions/CreateQuestionPage/CreateQuestionPage.module.css new file mode 100644 index 0000000000..4d8520e10d --- /dev/null +++ b/peer-prep/src/pages/Questions/CreateQuestionPage/CreateQuestionPage.module.css @@ -0,0 +1,9 @@ +.quillEditor { + border-radius: var(--mantine-radius-sm); + border: 1px solid var(--mantine-color-gray-2) !important; +} + +.testCaseHeader { + font-size: var(--input-label-size, var(--mantine-font-size-sm)); + margin-top: 12px; +} diff --git a/peer-prep/src/pages/Questions/CreateQuestionPage/CreateQuestionPage.tsx b/peer-prep/src/pages/Questions/CreateQuestionPage/CreateQuestionPage.tsx new file mode 100644 index 0000000000..e903c53fb3 --- /dev/null +++ b/peer-prep/src/pages/Questions/CreateQuestionPage/CreateQuestionPage.tsx @@ -0,0 +1,278 @@ +import { useEffect, useState } from "react"; +import { + TextInput, + Select, + Textarea, + Button, + Container, + Stack, + Center, + Input, + Stepper, + Text, + MultiSelect, + Card, + Switch, + Flex, + Divider, +} from "@mantine/core"; +import ReactQuill from "react-quill"; +import "react-quill/dist/quill.snow.css"; + +import classes from "./CreateQuestionPage.module.css"; +import { Question, QuestionOlsd, TestCase } from "../../../types/question"; +import useApi, { QuestionServerResponse, SERVICE } from "../../../hooks/useApi"; +import { notifications } from "@mantine/notifications"; + +export default function CreateQuestionPage() { + const [name, setName] = useState(""); + const [difficulty, setDifficulty] = useState("EASY"); + const [categories, setCategories] = useState([]); + const [description, setDescription] = useState(""); + const [testCases, setTestCases] = useState([]); + + const [fetchedCategories, setFetchedCategories] = useState< + { value: string; label: string }[] + >([]); + + // Mapping for difficulty display + const difficultyOptions = [ + { value: "EASY", label: "Easy" }, + { value: "MEDIUM", label: "Medium" }, + { value: "HARD", label: "Hard" }, + ]; + + useEffect(() => { + fetchCategories(); + }, []); + + const { fetchData, isLoading, error } = useApi(); + + const fetchCategories = async () => { + try { + const response = await fetchData>( + "/question-service/categories", + SERVICE.QUESTION + ); + + if (response.success) { + const categories = response.data; + const transformedCategories = categories.map((category: string) => ({ + value: category.toUpperCase(), + label: + category.charAt(0).toUpperCase() + category.slice(1).toLowerCase(), + })); + setFetchedCategories(transformedCategories); + } else { + notifications.show({ + message: + response.message || + "Error fetching categories, please try again later.", + color: "red", + }); + } + } catch (error: any) { + console.error("Error fetching categories", error); + notifications.show({ + message: error.message, + color: "red", + }); + } + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + + // Remove _id from test cases + const updatedTestCases = testCases.map(({ _id, ...rest }) => ({ + ...rest, + })); + + try { + const response = await fetchData>( + "/question-service", + SERVICE.QUESTION, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + title: name, + description: { testDescription: description }, + categories, + difficulty, + testCases: updatedTestCases, + }), + } + ); + + if (response.success) { + alert("Question created successfully!"); + window.location.href = "/questions"; + } else { + notifications.show({ + message: + response.message || + "Error creating question, please try again later.", + color: "red", + }); + } + } catch (error: any) { + console.log("Error creating question:", error); + notifications.show({ + message: error.message, + color: "red", + }); + } + }; + + const addTestCase = () => { + setTestCases([ + ...testCases, + { testCode: "", isPublic: false, meta: {}, expectedOutput: "" }, + ]); + }; + + const removeTestCase = (index: number) => { + setTestCases(testCases.filter((_, i) => i !== index)); + }; + + const handleTestCaseChange = ( + index: number, + field: keyof TestCase, + value: any + ) => { + const updatedTestCases = [...testCases]; + // @ts-ignore + updatedTestCases[index][field] = value; + setTestCases(updatedTestCases); + }; + + const [active, setActive] = useState(1); + + return ( + +

Add New Question

+
+ + setName(event.currentTarget.value)} + required + /> +