diff --git a/.eslintrc.cjs b/.eslintrc.cjs index b067d8d..66affe6 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -28,6 +28,7 @@ module.exports = { rules: { "import/no-extraneous-dependencies": 0, "react/function-component-definition": 0, + "react/jsx-props-no-spreading": 0, 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], 'sort-imports': ['error', {ignoreCase: true, ignoreDeclarationSort: true}], 'import/order': [ diff --git a/package-lock.json b/package-lock.json index 01cbe00..da6f250 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,11 @@ "name": "graphiql", "version": "0.0.0", "dependencies": { + "@hookform/resolvers": "^3.3.2", "@lit/react": "^1.0.2", "@material/web": "^1.0.1", "@types/react-test-renderer": "^18.0.7", + "firebase": "^10.7.1", "overlayscrollbars": "^2.4.5", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -27,6 +29,7 @@ "@types/node": "^20.10.3", "@types/react": "^18.2.37", "@types/react-dom": "^18.2.15", + "@types/toastify-js": "^1.12.3", "@typescript-eslint/eslint-plugin": "^6.13.2", "@typescript-eslint/parser": "^6.13.2", "@vitejs/plugin-react": "^4.2.0", @@ -891,6 +894,541 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@fastify/busboy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz", + "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@firebase/analytics": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.0.tgz", + "integrity": "sha512-Locv8gAqx0e+GX/0SI3dzmBY5e9kjVDtD+3zCFLJ0tH2hJwuCAiL+5WkHuxKj92rqQj/rvkBUCfA1ewlX2hehg==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.6.tgz", + "integrity": "sha512-4MqpVLFkGK7NJf/5wPEEP7ePBJatwYpyjgJ+wQHQGHfzaCDgntOnl9rL2vbVGGKCnRqWtZDIWhctB86UWXaX2Q==", + "dependencies": { + "@firebase/analytics": "0.10.0", + "@firebase/analytics-types": "0.8.0", + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-types": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.0.tgz", + "integrity": "sha512-iRP+QKI2+oz3UAh4nPEq14CsEjrjD6a5+fuypjScisAh9kXKFvdJOZJDwk7kikLvWVLGEs9+kIUS4LPQV7VZVw==" + }, + "node_modules/@firebase/app": { + "version": "0.9.25", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.9.25.tgz", + "integrity": "sha512-fX22gL5USXhOK21Hlh3oTeOzQZ6th6S2JrjXNEpBARmwzuUkqmVGVdsOCIFYIsLpK0dQE3o8xZnLrRg5wnzZ/g==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "idb": "7.1.1", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/app-check": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.8.1.tgz", + "integrity": "sha512-zi3vbM5tb/eGRWyiqf+1DXbxFu9Q07dnm46rweodgUpH9B8svxYkHfNwYWx7F5mjHU70SQDuaojH1We5ws9OKA==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/app-check-compat": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.3.8.tgz", + "integrity": "sha512-EaETtChR4UgMokJFw+r6jfcIyCTUZSe0a6ivF37D9MxlG9G3wzK1COyXgxoX96GzXmDPc2aubX4PxCrdVHhrnA==", + "dependencies": { + "@firebase/app-check": "0.8.1", + "@firebase/app-check-types": "0.5.0", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.0.tgz", + "integrity": "sha512-xAxHPZPIgFXnI+vb4sbBjZcde7ZluzPPaSK7Lx3/nmuVk4TjZvnL8ONnkd4ERQKL8WePQySU+pRcWkh8rDf5Sg==" + }, + "node_modules/@firebase/app-check-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.0.tgz", + "integrity": "sha512-uwSUj32Mlubybw7tedRzR24RP8M8JUVR3NPiMk3/Z4bCmgEKTlQBwMXrehDAZ2wF+TsBq0SN1c6ema71U/JPyQ==" + }, + "node_modules/@firebase/app-compat": { + "version": "0.2.25", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.25.tgz", + "integrity": "sha512-B/JtCp1FsTuzlh1tIGQpYM2AXps21/zlzpFsk5LRsROOTRhBcR2N45AyaONPFD06C0yS0Tw19foxADzHyOSC3A==", + "dependencies": { + "@firebase/app": "0.9.25", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.0.tgz", + "integrity": "sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q==" + }, + "node_modules/@firebase/auth": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.5.1.tgz", + "integrity": "sha512-sVi7rq2YneLGJFqHa5S6nDfCHix9yuVV3RLhj/pWPlB4a36ofXal4E6PJwpeMc8uLjWEr1aovYN1jkXWNB6Avw==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0", + "undici": "5.26.5" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^1.18.1" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@firebase/auth-compat": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.5.1.tgz", + "integrity": "sha512-rgDZnrDoekRvtzXVji8Z61wxxkof6pTkjYEkybILrjM8tGP9tx4xa9qGpF4ax3AzF+rKr7mIa9NnoXEK4UNqmQ==", + "dependencies": { + "@firebase/auth": "1.5.1", + "@firebase/auth-types": "0.12.0", + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0", + "undici": "5.26.5" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.1.tgz", + "integrity": "sha512-VOaGzKp65MY6P5FI84TfYKBXEPi6LmOCSMMzys6o2BN2LOsqy7pCuZCup7NYnfbk5OkkQKzvIfHOzTm0UDpkyg==" + }, + "node_modules/@firebase/auth-types": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.12.0.tgz", + "integrity": "sha512-pPwaZt+SPOshK8xNoiQlK5XIrS97kFYc3Rc7xmy373QsOJ9MmqXxLaYssP5Kcds4wd2qK//amx/c+A8O2fVeZA==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/component": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.4.tgz", + "integrity": "sha512-rLMyrXuO9jcAUCaQXCMjCMUsWrba5fzHlNK24xz5j2W6A/SRmK8mZJ/hn7V0fViLbxC0lPMtrK1eYzk6Fg03jA==", + "dependencies": { + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.2.tgz", + "integrity": "sha512-8X6NBJgUQzDz0xQVaCISoOLINKat594N2eBbMR3Mu/MH/ei4WM+aAMlsNzngF22eljXu1SILP5G3evkyvsG3Ng==", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.0", + "@firebase/auth-interop-types": "0.2.1", + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-1.0.2.tgz", + "integrity": "sha512-09ryJnXDvuycsxn8aXBzLhBTuCos3HEnCOBWY6hosxfYlNCGnLvG8YMlbSAt5eNhf7/00B095AEfDsdrrLjxqA==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/database": "1.0.2", + "@firebase/database-types": "1.0.0", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.0.tgz", + "integrity": "sha512-SjnXStoE0Q56HcFgNQ+9SsmJc0c8TqGARdI/T44KXy+Ets3r6x/ivhQozT66bMnCEjJRywYoxNurRTMlZF8VNg==", + "dependencies": { + "@firebase/app-types": "0.9.0", + "@firebase/util": "1.9.3" + } + }, + "node_modules/@firebase/firestore": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.4.0.tgz", + "integrity": "sha512-VeDXD9PUjvcWY1tInBOMTIu2pijR3YYy+QAe5cxCo1Q1vW+aA/mpQHhebPM1J6b4Zd1MuUh8xpBRvH9ujKR56A==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "@firebase/webchannel-wrapper": "0.10.5", + "@grpc/grpc-js": "~1.9.0", + "@grpc/proto-loader": "^0.7.8", + "tslib": "^2.1.0", + "undici": "5.26.5" + }, + "engines": { + "node": ">=10.10.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.23.tgz", + "integrity": "sha512-uUTBiP0GLVBETaOCfB11d33OWB8x1r2G1Xrl0sRK3Va0N5LJ/GRvKVSGfM7VScj+ypeHe8RpdwKoCqLpN1e+uA==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/firestore": "4.4.0", + "@firebase/firestore-types": "3.0.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/firestore-types": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.0.tgz", + "integrity": "sha512-Meg4cIezHo9zLamw0ymFYBD4SMjLb+ZXIbuN7T7ddXN6MGoICmOTq3/ltdCGoDCS2u+H1XJs2u/cYp75jsX9Qw==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/functions": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.11.0.tgz", + "integrity": "sha512-n1PZxKnJ++k73Q8khTPwihlbeKo6emnGzE0hX6QVQJsMq82y/XKmNpw2t/q30VJgwaia3ZXU1fd1C5wHncL+Zg==", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.0", + "@firebase/auth-interop-types": "0.2.1", + "@firebase/component": "0.6.4", + "@firebase/messaging-interop-types": "0.2.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0", + "undici": "5.26.5" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.3.6.tgz", + "integrity": "sha512-RQpO3yuHtnkqLqExuAT2d0u3zh8SDbeBYK5EwSCBKI9mjrFeJRXBnd3pEG+x5SxGJLy56/5pQf73mwt0OuH5yg==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/functions": "0.11.0", + "@firebase/functions-types": "0.6.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/functions-types": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.0.tgz", + "integrity": "sha512-hfEw5VJtgWXIRf92ImLkgENqpL6IWpYaXVYiRkFY1jJ9+6tIhWM7IzzwbevwIIud/jaxKVdRzD7QBWfPmkwCYw==" + }, + "node_modules/@firebase/installations": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.4.tgz", + "integrity": "sha512-u5y88rtsp7NYkCHC3ElbFBrPtieUybZluXyzl7+4BsIz4sqb4vSAuwHEUgCgCeaQhvsnxDEU6icly8U9zsJigA==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "idb": "7.0.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.4.tgz", + "integrity": "sha512-LI9dYjp0aT9Njkn9U4JRrDqQ6KXeAmFbRC0E7jI7+hxl5YmRWysq5qgQl22hcWpTk+cm3es66d/apoDU/A9n6Q==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/installations-types": "0.5.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/installations-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.0.tgz", + "integrity": "sha512-9DP+RGfzoI2jH7gY4SlzqvZ+hr7gYzPODrbzVD82Y12kScZ6ZpRg/i3j6rleto8vTFC8n6Len4560FnV1w2IRg==", + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/installations/node_modules/idb": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.0.1.tgz", + "integrity": "sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg==" + }, + "node_modules/@firebase/logger": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.0.tgz", + "integrity": "sha512-eRKSeykumZ5+cJPdxxJRgAC3G5NknY2GwEbKfymdnXtnT0Ucm4pspfR6GT4MUQEDuJwRVbVcSx85kgJulMoFFA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/messaging": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.5.tgz", + "integrity": "sha512-i/rrEI2k9ueFhdIr8KQsptWGskrsnkC5TkohCTrJKz9P0C/PbNv14IAMkwhMJTqIur5VwuOnrUkc9Kdz7awekw==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/messaging-interop-types": "0.2.0", + "@firebase/util": "1.9.3", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.5.tgz", + "integrity": "sha512-qHQZxm4hEG8/HFU/ls5/bU+rpnlPDoZoqi3ATMeb6s4hovYV9+PfV5I7ZrKV5eFFv47Hx1PWLe5uPnS4e7gMwQ==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/messaging": "0.12.5", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.0.tgz", + "integrity": "sha512-ujA8dcRuVeBixGR9CtegfpU4YmZf3Lt7QYkcj693FFannwNuZgfAYaTmbJ40dtjB81SAu6tbFPL9YLNT15KmOQ==" + }, + "node_modules/@firebase/performance": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.6.4.tgz", + "integrity": "sha512-HfTn/bd8mfy/61vEqaBelNiNnvAbUtME2S25A67Nb34zVuCSCRIX4SseXY6zBnOFj3oLisaEqhVcJmVPAej67g==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.4.tgz", + "integrity": "sha512-nnHUb8uP9G8islzcld/k6Bg5RhX62VpbAb/Anj7IXs/hp32Eb2LqFPZK4sy3pKkBUO5wcrlRWQa6wKOxqlUqsg==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/performance": "0.6.4", + "@firebase/performance-types": "0.2.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-types": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.0.tgz", + "integrity": "sha512-kYrbr8e/CYr1KLrLYZZt2noNnf+pRwDq2KK9Au9jHrBMnb0/C9X9yWSXmZkFt4UIdsQknBq8uBB7fsybZdOBTA==" + }, + "node_modules/@firebase/remote-config": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.4.4.tgz", + "integrity": "sha512-x1ioTHGX8ZwDSTOVp8PBLv2/wfwKzb4pxi0gFezS5GCJwbLlloUH4YYZHHS83IPxnua8b6l0IXUaWd0RgbWwzQ==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/installations": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.4.tgz", + "integrity": "sha512-FKiki53jZirrDFkBHglB3C07j5wBpitAaj8kLME6g8Mx+aq7u9P7qfmuSRytiOItADhWUj7O1JIv7n9q87SuwA==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/remote-config": "0.4.4", + "@firebase/remote-config-types": "0.3.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.3.0.tgz", + "integrity": "sha512-RtEH4vdcbXZuZWRZbIRmQVBNsE7VDQpet2qFvq6vwKLBIQRQR5Kh58M4ok3A3US8Sr3rubYnaGqZSurCwI8uMA==" + }, + "node_modules/@firebase/storage": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.12.0.tgz", + "integrity": "sha512-SGs02Y/mmWBRsqZiYLpv4Sf7uZYZzMWVNN+aKiDqPsFBCzD6hLvGkXz+u98KAl8FqcjgB8BtSu01wm4pm76KHA==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0", + "undici": "5.26.5" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.3.3.tgz", + "integrity": "sha512-WNtjYPhpOA1nKcRu5lIodX0wZtP8pI0VxDJnk6lr+av7QZNS1s6zvr+ERDTve+Qu4Hq/ZnNaf3kBEQR2ccXn6A==", + "dependencies": { + "@firebase/component": "0.6.4", + "@firebase/storage": "0.12.0", + "@firebase/storage-types": "0.8.0", + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.0.tgz", + "integrity": "sha512-isRHcGrTs9kITJC0AVehHfpraWFui39MPaU7Eo8QfWlqW7YPymBmRgjDrlOgFdURh6Cdeg07zmkLP5tzTKRSpg==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/util": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.3.tgz", + "integrity": "sha512-DY02CRhOZwpzO36fHpuVysz6JZrscPiBXD0fXp6qSrL9oNOx5KWICKdR95C0lSITzxp0TZosVyHqzatE8JbcjA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.10.5.tgz", + "integrity": "sha512-eSkJsnhBWv5kCTSU1tSUVl9mpFu+5NXXunZc83le8GMjMlsWwQArSc7cJJ4yl+aDFY0NGLi0AjZWMn1axOrkRg==" + }, + "node_modules/@grpc/grpc-js": { + "version": "1.9.12", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.12.tgz", + "integrity": "sha512-Um5MBuge32TS3lAKX02PGCnFM4xPT996yLgZNb5H03pn6NyJ4Iwn5YcPq6Jj9yxGRk7WOgaZFtVRH5iTdYBeUg==", + "dependencies": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.10.tgz", + "integrity": "sha512-CAqDfoaQ8ykFd9zqBDn4k6iWT9loLAlc2ETmDFS9JCD70gDcnA4L3AFEo2iV7KyAtAAHFW9ftq1Fz+Vsgq80RQ==", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.4", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@hookform/resolvers": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.2.tgz", + "integrity": "sha512-Tw+GGPnBp+5DOsSg4ek3LCPgkBOuOgS5DsDV7qsWNH9LZc433kgsWICjlsh2J9p04H2K66hsXPPb9qn9ILdUtA==", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.6.0.tgz", @@ -1065,6 +1603,60 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, "node_modules/@remix-run/router": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.13.1.tgz", @@ -1582,7 +2174,6 @@ "version": "20.10.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.3.tgz", "integrity": "sha512-XJavIpZqiXID5Yxnxv3RUDKTN5b81ddNC3ecsA0SoFXz/QU8OGBwZGMomiq0zw+uuqbL/krztv/DINAQ/EV4gg==", - "dev": true, "dependencies": { "undici-types": "~5.26.4" } @@ -1630,6 +2221,12 @@ "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", "dev": true }, + "node_modules/@types/toastify-js": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/@types/toastify-js/-/toastify-js-1.12.3.tgz", + "integrity": "sha512-9RjLlbAHMSaae/KZNHGv19VG4gcLIm3YjvacCXBtfMfYn26h76YP5oxXI8k26q4iKXCB9LNfv18lsoS0JnFPTg==", + "dev": true + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -2059,7 +2656,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -2602,6 +3198,91 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cliui/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/clsx": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", @@ -3173,7 +3854,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, "engines": { "node": ">=6" } @@ -3920,6 +4600,17 @@ "reusify": "^1.0.4" } }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -3944,6 +4635,39 @@ "node": ">=8" } }, + "node_modules/firebase": { + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-10.7.1.tgz", + "integrity": "sha512-Mlt7y7zQ43FtKp4SCyYie3tnrOL3UMF2XXiV4ZXMrC0d0wtcOYmABuybhkJpJCKILpdekxr39wjnaai0DZlWFg==", + "dependencies": { + "@firebase/analytics": "0.10.0", + "@firebase/analytics-compat": "0.2.6", + "@firebase/app": "0.9.25", + "@firebase/app-check": "0.8.1", + "@firebase/app-check-compat": "0.3.8", + "@firebase/app-compat": "0.2.25", + "@firebase/app-types": "0.9.0", + "@firebase/auth": "1.5.1", + "@firebase/auth-compat": "0.5.1", + "@firebase/database": "1.0.2", + "@firebase/database-compat": "1.0.2", + "@firebase/firestore": "4.4.0", + "@firebase/firestore-compat": "0.3.23", + "@firebase/functions": "0.11.0", + "@firebase/functions-compat": "0.3.6", + "@firebase/installations": "0.6.4", + "@firebase/installations-compat": "0.2.4", + "@firebase/messaging": "0.12.5", + "@firebase/messaging-compat": "0.2.5", + "@firebase/performance": "0.6.4", + "@firebase/performance-compat": "0.2.4", + "@firebase/remote-config": "0.4.4", + "@firebase/remote-config-compat": "0.2.4", + "@firebase/storage": "0.12.0", + "@firebase/storage-compat": "0.3.3", + "@firebase/util": "1.9.3" + } + }, "node_modules/flat-cache": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", @@ -4071,6 +4795,14 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-east-asian-width": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz", @@ -4355,6 +5087,11 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==" + }, "node_modules/http-proxy-agent": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", @@ -4417,6 +5154,11 @@ "node": ">=0.10.0" } }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==" + }, "node_modules/ignore": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", @@ -5334,6 +6076,11 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -5429,6 +6176,11 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -6367,6 +7119,29 @@ "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==" }, + "node_modules/protobufjs": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.5.tgz", + "integrity": "sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -6560,6 +7335,14 @@ "url": "https://github.com/sponsors/mysticatea" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -6851,6 +7634,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/safe-regex-test": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", @@ -7207,7 +8009,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -7738,11 +8539,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.26.5.tgz", + "integrity": "sha512-cSb4bPFd5qgR7qr2jYAi0hlX9n5YKK2ONKkLFkxl+v/9BvC0sOpZjBHDBSXc5lWAf5ty9oZdRXytBIHzgUcerw==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/undici-types": { "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 + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/universalify": { "version": "0.2.0", @@ -8057,6 +8868,27 @@ "node": ">=12" } }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/whatwg-encoding": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", @@ -8270,6 +9102,14 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -8285,6 +9125,57 @@ "node": ">= 14" } }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", diff --git a/package.json b/package.json index 996b8e2..4dd22e3 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,11 @@ "type-check": "tsc --noEmit" }, "dependencies": { + "@hookform/resolvers": "^3.3.2", "@lit/react": "^1.0.2", "@material/web": "^1.0.1", "@types/react-test-renderer": "^18.0.7", + "firebase": "^10.7.1", "overlayscrollbars": "^2.4.5", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -35,6 +37,7 @@ "@types/node": "^20.10.3", "@types/react": "^18.2.37", "@types/react-dom": "^18.2.15", + "@types/toastify-js": "^1.12.3", "@typescript-eslint/eslint-plugin": "^6.13.2", "@typescript-eslint/parser": "^6.13.2", "@vitejs/plugin-react": "^4.2.0", diff --git a/src/App.tsx b/src/App.tsx index 680300f..a87a60d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,35 +1,18 @@ -import { createHashRouter, RouterProvider } from 'react-router-dom'; +import { RouterProvider } from 'react-router-dom'; -import MainLayout from '@/layouts/MainLayout'; -import LoginPage from '@pages/LoginPage'; -import MainPage from '@pages/MainPage'; -import WelcomePage from '@pages/WelcomePage'; +import router from '@/router/router'; -import ROUTES from './shared/constatns/routes'; - -const router = createHashRouter([ - { - path: '/', - element: , - children: [ - { - path: ROUTES.WELCOME_PAGE, - element: , - }, - { - path: ROUTES.LOGIN, - element: , - }, - { - path: ROUTES.MAIN, - element: , - }, - ], - }, -]); +import AuthProvider from './shared/Context/AuthContext'; +import LanguageProvider from './shared/Context/LanguageContext'; const App = () => { - return ; + return ( + + + + + + ); }; export default App; diff --git a/src/components/.gitkeep b/src/components/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/components/loginReg/FormInput.ts b/src/components/loginReg/FormInput.ts new file mode 100644 index 0000000..ac1b076 --- /dev/null +++ b/src/components/loginReg/FormInput.ts @@ -0,0 +1,12 @@ +import React from 'react'; + +import { createComponent } from '@lit/react'; +import { MdOutlinedTextField } from '@material/web/textfield/outlined-text-field'; + +const FormInput = createComponent({ + react: React, + tagName: 'md-outlined-text-field', + elementClass: MdOutlinedTextField, +}); + +export default FormInput; diff --git a/src/components/loginReg/PassVisibilityIcon.tsx b/src/components/loginReg/PassVisibilityIcon.tsx new file mode 100644 index 0000000..71d812e --- /dev/null +++ b/src/components/loginReg/PassVisibilityIcon.tsx @@ -0,0 +1,28 @@ +import React from 'react'; + +import { createComponent } from '@lit/react'; +import { MdIcon } from '@material/web/icon/icon'; +import { MdIconButton } from '@material/web/iconbutton/icon-button'; + +const Icon = createComponent({ + react: React, + tagName: 'md-icon', + elementClass: MdIcon, +}); + +const IconSlot = createComponent({ + react: React, + tagName: 'md-icon-button', + elementClass: MdIconButton, +}); + +const PassVisibilityIcon = ({ onClick }: { onClick: () => void }) => { + return ( + + visibility + visibility_off + + ); +}; + +export default PassVisibilityIcon; diff --git a/src/components/loginReg/SubmitBtn.ts b/src/components/loginReg/SubmitBtn.ts new file mode 100644 index 0000000..dac7402 --- /dev/null +++ b/src/components/loginReg/SubmitBtn.ts @@ -0,0 +1,12 @@ +import React from 'react'; + +import { createComponent } from '@lit/react'; +import { MdFilledTonalButton } from '@material/web/button/filled-tonal-button'; + +const SubmitBtn = createComponent({ + react: React, + tagName: 'md-filled-tonal-button', + elementClass: MdFilledTonalButton, +}); + +export default SubmitBtn; diff --git a/src/firebase.ts b/src/firebase.ts new file mode 100644 index 0000000..142c491 --- /dev/null +++ b/src/firebase.ts @@ -0,0 +1,14 @@ +import { initializeApp } from 'firebase/app'; + +const firebaseConfig = { + apiKey: 'AIzaSyAijnqgoeQIQJu_lKQ6fgNC7a0fIEuND2c', + authDomain: 'graphiql-app-47127.firebaseapp.com', + projectId: 'graphiql-app-47127', + storageBucket: 'graphiql-app-47127.appspot.com', + messagingSenderId: '1033759243197', + appId: '1:1033759243197:web:3757214e041ae1ab0bf0d4', +}; + +const initFirebaseApp = () => initializeApp(firebaseConfig); + +export default initFirebaseApp; diff --git a/src/layouts/MainLayout.tsx b/src/layouts/MainLayout.tsx index 052fff8..cd5cba9 100644 --- a/src/layouts/MainLayout.tsx +++ b/src/layouts/MainLayout.tsx @@ -2,12 +2,11 @@ import { Outlet } from 'react-router-dom'; const MainLayout = () => { return ( -
-
Here will be header
-
+
+
-
Here will be footer
+
Here will be footer
); }; diff --git a/src/locales/en.ts b/src/locales/en.ts index 0ebcf14..eb6eb15 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -1,4 +1,23 @@ const en = { + loginPage: { + title: 'Log in', + subtitle: 'to continue to GraphiQL 🚀', + emailPlaceHold: 'Email', + passPlaceHold: 'Password', + btnTitle: 'Log in', + linkClue: "Don't have an account yet?", + linkTitle: 'Sign up', + }, + signUpPage: { + title: 'Sign in', + subtitle: 'to continue to GraphiQL 🚀', + emailPlaceHold: 'Email', + passPlaceHold: 'Password', + confPassPlaceHold: 'Confirm Password', + btnTitle: 'Sign up', + linkClue: 'Already have an account?', + linkTitle: 'Log in', + }, header: { h1: { h2: 'english' } }, button: 'Press me', }; diff --git a/src/locales/ru.ts b/src/locales/ru.ts index 9b3a7fb..6ef84a1 100644 --- a/src/locales/ru.ts +++ b/src/locales/ru.ts @@ -1,4 +1,23 @@ const ru = { + loginPage: { + title: 'Войти', + subtitle: 'чтобы воспользоваться GraphiQL 🚀', + emailPlaceHold: 'Эл. почта', + passPlaceHold: 'Пароль', + btnTitle: 'Войти', + linkClue: 'Ещё нет аккаунта?', + linkTitle: 'Зарегистрироваться', + }, + signUpPage: { + title: 'Зарегистрироваться', + subtitle: 'чтобы воспользоваться GraphiQL 🚀', + emailPlaceHold: 'Эл. почта', + passPlaceHold: 'Пароль', + confPassPlaceHold: 'Подтв. пароль', + btnTitle: 'Зарегистрироваться', + linkClue: 'Уже есть аккаунт?', + linkTitle: 'Войти', + }, header: { h1: { h2: 'русский' } }, button: 'Нажми', }; diff --git a/src/main.tsx b/src/main.tsx index afd800c..2992677 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -4,14 +4,13 @@ import ReactDOM from 'react-dom/client'; import App from '@/App'; -import LanguageProvider from './shared/Context/LanguageContext'; - +import initFirebaseApp from './firebase'; import '@/styles/index.css'; +initFirebaseApp(); + ReactDOM.createRoot(document.getElementById('root')!).render( - - - + , ); diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index dbd1dbd..5076513 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -1,5 +1,103 @@ -const LoginPage = () => { - return
Here is my fancy login page!
; -}; +import { useState } from 'react'; -export default LoginPage; +import { yupResolver } from '@hookform/resolvers/yup'; +import { TextFieldType } from '@material/web/textfield/outlined-text-field'; +import { getAuth, signInWithEmailAndPassword } from 'firebase/auth'; +import { useForm } from 'react-hook-form'; +import { Link, useNavigate } from 'react-router-dom'; + +import PassVisibilityIcon from '@/components/loginReg/PassVisibilityIcon'; +import AUTH_ERRORS from '@/shared/constants/authErrors'; +import ROUTES from '@/shared/constants/routes'; +import { loginValidationSchema } from '@/shared/constants/validationSchema'; +import useAuth from '@/shared/Context/authHook'; +import useLanguage from '@/shared/Context/hooks'; +import notationLocalizer from '@/shared/helpers/notationLocalizer'; +import switchPassType from '@/shared/helpers/switchPassType'; +import toastifyNotation from '@/shared/helpers/toastifyNotation'; +import { ErrorType, TextInputProps } from '@/shared/types'; +import FormInput from '@components/loginReg/FormInput'; +import SubmitBtn from '@components/loginReg/SubmitBtn'; + +export default function LoginPage() { + const navigate = useNavigate(); + const [passType, setPassType] = useState('password'); + const { translation, language } = useLanguage(); + const { title, subtitle, emailPlaceHold, passPlaceHold, btnTitle, linkClue, linkTitle } = translation.loginPage; + const { logInAuth } = useAuth(); + + const { + register, + handleSubmit, + formState: { errors, isValid }, + reset, + } = useForm({ + resolver: yupResolver(loginValidationSchema), + mode: 'all', + }); + + async function onSubmit({ email, password }: { email: string; password: string }) { + const auth = getAuth(); + try { + const { user } = await signInWithEmailAndPassword(auth, email, password); + if (user) { + logInAuth(user.email as string); + reset(); + navigate(`/${ROUTES.MAIN}`); + } + return null; + } catch (e) { + if ((e as ErrorType).code === AUTH_ERRORS.INVALID_EMAIL) + return toastifyNotation(notationLocalizer(language, 'code8')); + if ((e as ErrorType).code === AUTH_ERRORS.INVALID_PASS) + return toastifyNotation(notationLocalizer(language, 'code9')); + return toastifyNotation(notationLocalizer(language, 'code11')); + } + } + + return ( +
+
+

{title}

+

{subtitle}

+
+
+ +

+ {notationLocalizer(language, errors.email?.message)} +

+
+
+ + setPassType((prev) => switchPassType(prev))} /> + +

+ {notationLocalizer(language, errors.password?.message)} +

+
+ + {btnTitle} + +
+

+ {linkClue}{' '} + + {linkTitle} + +

+
+
+ ); +} diff --git a/src/pages/SignUpPage.tsx b/src/pages/SignUpPage.tsx new file mode 100644 index 0000000..d2067e1 --- /dev/null +++ b/src/pages/SignUpPage.tsx @@ -0,0 +1,116 @@ +import { useState } from 'react'; + +import { yupResolver } from '@hookform/resolvers/yup'; +import { TextFieldType } from '@material/web/textfield/outlined-text-field'; +import { createUserWithEmailAndPassword, getAuth } from 'firebase/auth'; +import { useForm } from 'react-hook-form'; +import { Link, useNavigate } from 'react-router-dom'; + +import FormInput from '@/components/loginReg/FormInput'; +import PassVisibilityIcon from '@/components/loginReg/PassVisibilityIcon'; +import SubmitBtn from '@/components/loginReg/SubmitBtn'; +import AUTH_ERRORS from '@/shared/constants/authErrors'; +import ROUTES from '@/shared/constants/routes'; +import { regValidationSchema } from '@/shared/constants/validationSchema'; +import useAuth from '@/shared/Context/authHook'; +import useLanguage from '@/shared/Context/hooks'; +import notationLocalizer from '@/shared/helpers/notationLocalizer'; +import switchPassType from '@/shared/helpers/switchPassType'; +import toastifyNotation from '@/shared/helpers/toastifyNotation'; +import { ErrorType, TextInputProps } from '@/shared/types'; + +export default function SignUpPage() { + const [passType, setPassType] = useState('password'); + const [confPassType, setConfPassType] = useState('password'); + const navigate = useNavigate(); + const { logInAuth } = useAuth(); + const { translation, language } = useLanguage(); + const { title, subtitle, emailPlaceHold, passPlaceHold, btnTitle, linkClue, linkTitle, confPassPlaceHold } = + translation.signUpPage; + const { + register, + handleSubmit, + formState: { errors, isValid }, + reset, + } = useForm({ + resolver: yupResolver(regValidationSchema), + mode: 'all', + }); + + async function onSubmit({ email, password }: { email: string; password: string }) { + const auth = getAuth(); + try { + const { user } = await createUserWithEmailAndPassword(auth, email, password); + if (user) { + reset(); + logInAuth(user.email as string); + navigate(`/${ROUTES.MAIN}`); + } + return null; + } catch (e) { + if ((e as ErrorType).code === AUTH_ERRORS.EMAIL_IN_USE) + return toastifyNotation(notationLocalizer(language, 'code10')); + return toastifyNotation(notationLocalizer(language, 'code11')); + } + } + + return ( +
+
+

{title}

+

{subtitle}

+
+
+ +

+ {notationLocalizer(language, errors.email?.message)} +

+
+
+ + setPassType((prev) => switchPassType(prev))} /> + +

+ {notationLocalizer(language, errors.password?.message)} +

+
+
+ + setConfPassType((prev) => switchPassType(prev))} /> + +

+ {notationLocalizer(language, errors.confirmPassword?.message)} +

+
+ + {btnTitle} + +
+

+ {linkClue}{' '} + + {linkTitle} + +

+
+
+ ); +} diff --git a/src/pages/WelcomePage.tsx b/src/pages/WelcomePage.tsx index 8ea3272..0391e7a 100644 --- a/src/pages/WelcomePage.tsx +++ b/src/pages/WelcomePage.tsx @@ -1,12 +1,14 @@ import { Link } from 'react-router-dom'; +import ROUTES from '@/shared/constants/routes'; + const WelcomePage = () => { return (
Here is my fancy welcome page!
- login - main page + login + main page
); diff --git a/src/router/AuthAllowedOnly.tsx b/src/router/AuthAllowedOnly.tsx new file mode 100644 index 0000000..5a46ed9 --- /dev/null +++ b/src/router/AuthAllowedOnly.tsx @@ -0,0 +1,12 @@ +import { Navigate } from 'react-router-dom'; + +import ROUTES from '@/shared/constants/routes'; +import useAuth from '@/shared/Context/authHook'; + +const AuthAllowedOnly = ({ children }: { children: JSX.Element }) => { + const { isAuth } = useAuth(); + if (!isAuth) return ; + return children; +}; + +export default AuthAllowedOnly; diff --git a/src/router/UnauthAllowedOnly.tsx b/src/router/UnauthAllowedOnly.tsx new file mode 100644 index 0000000..b358ea2 --- /dev/null +++ b/src/router/UnauthAllowedOnly.tsx @@ -0,0 +1,12 @@ +import { Navigate } from 'react-router-dom'; + +import ROUTES from '@/shared/constants/routes'; +import useAuth from '@/shared/Context/authHook'; + +const UnauthAllowedOnly = ({ children }: { children: JSX.Element }) => { + const { isAuth } = useAuth(); + if (isAuth) return ; + return children; +}; + +export default UnauthAllowedOnly; diff --git a/src/router/router.tsx b/src/router/router.tsx new file mode 100644 index 0000000..d03821d --- /dev/null +++ b/src/router/router.tsx @@ -0,0 +1,63 @@ +import { createHashRouter } from 'react-router-dom'; + +import MainLayout from '@/layouts/MainLayout'; +import LoginPage from '@/pages/LoginPage'; +import MainPage from '@/pages/MainPage'; +import SignUpPage from '@/pages/SignUpPage'; +import WelcomePage from '@/pages/WelcomePage'; +import ROUTES from '@/shared/constants/routes'; + +import AuthAllowedOnly from './AuthAllowedOnly'; +import UnauthAllowedOnly from './UnauthAllowedOnly'; + +const router = createHashRouter([ + { + path: '/', + element: , + children: [ + { + path: ROUTES.WELCOME_PAGE, + element: , + }, + { + path: ROUTES.AUTH, + children: [ + { + path: ROUTES.LOGIN, + element: ( + + + + ), + }, + { + path: ROUTES.SIGNUP, + element: ( + + + + ), + }, + { + index: true, + element: ( + + + + ), + }, + ], + }, + { + path: ROUTES.MAIN, + element: ( + + + + ), + }, + ], + }, +]); + +export default router; diff --git a/src/shared/.gitkeep b/src/shared/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/shared/Context/AuthContext.tsx b/src/shared/Context/AuthContext.tsx new file mode 100644 index 0000000..ef55dac --- /dev/null +++ b/src/shared/Context/AuthContext.tsx @@ -0,0 +1,38 @@ +import { createContext, useCallback, useMemo, useState } from 'react'; + +import { deleteAuthCookie, isAuthCookie, prepareAuthCookie } from '@shared/helpers/cookieHandlers'; + +type AuthApi = { + isAuth: boolean; + logInAuth: (email: string) => void; + logOutAuth: () => void; +}; + +export const AuthContext = createContext({} as AuthApi); + +const AuthProvider = ({ children }: { children: JSX.Element }) => { + const authCookie = isAuthCookie(); + const [isAuth, setIsAuth] = useState(authCookie); + + const logInAuth = useCallback((email: string) => { + document.cookie = prepareAuthCookie(email as string); + setIsAuth(true); + }, []); + + const logOutAuth = useCallback(() => { + deleteAuthCookie(); + setIsAuth(false); + }, []); + + const authApi = useMemo( + () => ({ + isAuth, + logInAuth, + logOutAuth, + }), + [isAuth, logInAuth, logOutAuth], + ); + return {children}; +}; + +export default AuthProvider; diff --git a/src/shared/Context/authHook.ts b/src/shared/Context/authHook.ts new file mode 100644 index 0000000..fcb12a2 --- /dev/null +++ b/src/shared/Context/authHook.ts @@ -0,0 +1,9 @@ +import { useContext } from 'react'; + +import { AuthContext } from './AuthContext'; + +const useAuth = () => { + return useContext(AuthContext); +}; + +export default useAuth; diff --git a/src/shared/constants/authErrors.ts b/src/shared/constants/authErrors.ts new file mode 100644 index 0000000..e18ed1d --- /dev/null +++ b/src/shared/constants/authErrors.ts @@ -0,0 +1,7 @@ +const AUTH_ERRORS = { + INVALID_EMAIL: 'auth/invalid-email', + INVALID_PASS: 'auth/invalid-credential', + EMAIL_IN_USE: 'auth/email-already-in-use', +} as const; + +export default AUTH_ERRORS; diff --git a/src/shared/constatns/routes.ts b/src/shared/constants/routes.ts similarity index 75% rename from src/shared/constatns/routes.ts rename to src/shared/constants/routes.ts index fd327e8..814c623 100644 --- a/src/shared/constatns/routes.ts +++ b/src/shared/constants/routes.ts @@ -1,6 +1,8 @@ const ROUTES = { WELCOME_PAGE: '/', + AUTH: 'auth', LOGIN: 'login', + SIGNUP: 'signup', MAIN: 'main', } as const; diff --git a/src/shared/constants/validationSchema.ts b/src/shared/constants/validationSchema.ts new file mode 100644 index 0000000..87fc464 --- /dev/null +++ b/src/shared/constants/validationSchema.ts @@ -0,0 +1,46 @@ +import * as Yup from 'yup'; + +const emailRegEx = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i; +const atLeastOneDigit = /[0-9]/; +const atLeastOneLowerCase = /[a-z]/; +const atLeastOneUpperCase = /[A-Z]/; +const atLeastOneSpecialCharacter = /[^\w\s]/g; + +const email = () => { + return { + email: Yup.string().matches(emailRegEx, { message: 'code1', excludeEmptyString: true }).required('code2'), + }; +}; + +const password = () => { + return { + password: Yup.string() + .min(8, 'code3') + .matches(atLeastOneDigit, 'code12') + .matches(atLeastOneLowerCase, 'code13') + .matches(atLeastOneUpperCase, 'code14') + .matches(atLeastOneSpecialCharacter, 'code15') + .required('code5'), + }; +}; + +const confirmPassword = () => { + return { + confirmPassword: Yup.string() + .required('code6') + .oneOf([Yup.ref('password')], 'code7'), + }; +}; + +const loginValidationSchema = Yup.object({ + ...email(), + ...password(), +}); + +const regValidationSchema = Yup.object({ + ...email(), + ...password(), + ...confirmPassword(), +}); + +export { loginValidationSchema, regValidationSchema }; diff --git a/src/shared/helpers/cookieHandlers.ts b/src/shared/helpers/cookieHandlers.ts new file mode 100644 index 0000000..29fc9d9 --- /dev/null +++ b/src/shared/helpers/cookieHandlers.ts @@ -0,0 +1,23 @@ +export function prepareAuthCookie(_email: string) { + return `userEmail=${_email}; max-age=3600`; +} + +export function deleteAuthCookie() { + document.cookie = 'userEmail=""; max-age=-1'; +} + +export function isAuthCookie() { + const cookies = document.cookie.split(';'); + let userEmail = false; + + cookies.forEach((cookie) => { + const [name] = cookie.trim().split('='); + if (name === 'userEmail') { + userEmail = true; + return null; + } + return null; + }); + + return userEmail; +} diff --git a/src/shared/helpers/notationLocalizer.ts b/src/shared/helpers/notationLocalizer.ts new file mode 100644 index 0000000..d973598 --- /dev/null +++ b/src/shared/helpers/notationLocalizer.ts @@ -0,0 +1,45 @@ +const enLocale = { + code1: 'Email must be email@example.com', + code2: 'Email is required', + code3: 'Minimum 8 symbols required', + code5: 'Password is required', + code6: 'Confirm password is required', + code7: 'Password mismatch', + code8: 'Error, wrong email', + code9: 'Error, wrong password', + code10: 'Error, email is occupied', + code11: 'Unexpected error have happened...', + code12: 'Must contain at least 1 digit', + code13: 'Must contain at least 1 lowercase letter', + code14: 'Must contain at least 1 uppercase letter', + code15: 'Must contain at least 1 special character', +}; +const ruLocale = { + code1: 'Эл. почта должна быть email@example.com', + code2: 'Эл. почта обязательна', + code3: 'Не менее 8 символов', + code5: 'Пароль обязателен', + code6: 'Подтверждение пароля обязательно', + code7: 'Пароли не совпадают', + code8: 'Ошибка, неверная эл. почта', + code9: 'Ошибка, неверный пароль', + code10: 'Ошибка, эл. почта занята', + code11: 'Произошла непредвиденная ошибка...', + code12: 'Должен содержать как минимум 1 цифру', + code13: 'Должен содержать как минимум 1 букву', + code14: 'Должен содержать как минимум 1 загл. букву', + code15: 'Должен содержать как минимум 1 спец. символ', +}; + +type Translation = { [key: string]: string }; + +export default function notationLocalizer(lang: string, errCode?: string): string { + if (!errCode) return ''; + + const TranslationFiles: Record = { + en: enLocale, + ru: ruLocale, + }; + + return TranslationFiles[lang][errCode]; +} diff --git a/src/shared/helpers/switchPassType.ts b/src/shared/helpers/switchPassType.ts new file mode 100644 index 0000000..ede956d --- /dev/null +++ b/src/shared/helpers/switchPassType.ts @@ -0,0 +1,3 @@ +export default function switchPassType(prevType: string) { + return prevType === 'password' ? 'text' : 'password'; +} diff --git a/src/shared/helpers/toastifyNotation.ts b/src/shared/helpers/toastifyNotation.ts new file mode 100644 index 0000000..7487cc8 --- /dev/null +++ b/src/shared/helpers/toastifyNotation.ts @@ -0,0 +1,12 @@ +import Toastify from 'toastify-js'; +import 'toastify-js/src/toastify.css'; + +const toastifyNotation = (message: string) => { + return Toastify({ + text: message, + duration: 1500, + position: 'center', + }).showToast(); +}; + +export default toastifyNotation; diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts new file mode 100644 index 0000000..6082702 --- /dev/null +++ b/src/shared/types/index.ts @@ -0,0 +1,7 @@ +import { MdOutlinedTextField } from '@material/web/textfield/outlined-text-field'; + +export type ErrorType = { + code: string; +}; + +export type TextInputProps = Partial>; diff --git a/src/test/cookieHandlers.test.ts b/src/test/cookieHandlers.test.ts new file mode 100644 index 0000000..e6201ab --- /dev/null +++ b/src/test/cookieHandlers.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from 'vitest'; + +import { deleteAuthCookie, isAuthCookie, prepareAuthCookie } from '@/shared/helpers/cookieHandlers'; + +describe('Testing of cookie handlers functions', () => { + it('It should write a cookie to document cookies', () => { + expect(document.cookie).toMatch(''); + document.cookie = prepareAuthCookie('test@gmail.com'); + expect(document.cookie).toMatch('userEmail=test@gmail.com'); + }); + it('It shoult delete a cookie after calling a function to do so', () => { + document.cookie = prepareAuthCookie('test@gmail.com'); + expect(document.cookie).toMatch('userEmail=test@gmail.com'); + deleteAuthCookie(); + expect(document.cookie).toMatch(''); + }); + it('Function should retrieve auth cookie value from cookies', () => { + expect(isAuthCookie()).toBe(false); + document.cookie = prepareAuthCookie('test@gmail.com'); + expect(isAuthCookie()).toBe(true); + }); +}); diff --git a/src/test/loginPage.test.tsx b/src/test/loginPage.test.tsx new file mode 100644 index 0000000..7319935 --- /dev/null +++ b/src/test/loginPage.test.tsx @@ -0,0 +1,32 @@ +import { act, fireEvent, screen, waitFor } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; + +import App from '@/App'; + +import userSetup from './setupTests'; + +describe('Testing for login page', () => { + it('Should render login page properly', async () => { + const { user } = userSetup(); + const loginLink = await screen.findByText('login'); + await act(async () => { + user.click(loginLink); + }); + const emailInput = await screen.findByPlaceholderText('Email'); + const passInput = await screen.findByPlaceholderText('Password'); + await act(async () => { + fireEvent.change(emailInput, { + target: { value: 'asdrogachev@gmail.com' }, + }); + fireEvent.change(passInput, { + target: { value: '698830Pa$$' }, + }); + }); + const submit = await screen.findByRole('button', { name: 'Log in' }); + waitFor(async () => { + await user.click(submit); + }); + expect((emailInput as HTMLInputElement).value).toMatch('asdrogachev@gmail.com'); + expect((passInput as HTMLInputElement).value).toMatch('698830Pa$$'); + }); +}); diff --git a/src/test/routing/authorizedLoginRoute.test.tsx b/src/test/routing/authorizedLoginRoute.test.tsx new file mode 100644 index 0000000..16bfa01 --- /dev/null +++ b/src/test/routing/authorizedLoginRoute.test.tsx @@ -0,0 +1,27 @@ +import { act, cleanup, screen } from '@testing-library/react'; +import { afterEach, describe, expect, it } from 'vitest'; + +import App from '@/App'; +import { prepareAuthCookie } from '@/shared/helpers/cookieHandlers'; + +import userSetup from '../setupTests'; + +afterEach(() => { + cleanup(); + document.body.innerHTML = ''; +}); + +describe('Testing the authorized login page route', () => { + it('If user is authorized, and tries to react login route - he should be redirected to main page.', async () => { + document.cookie = prepareAuthCookie('test@gmail.com'); + const { user } = userSetup(); + const loginLink = await screen.findByText('login'); + await act(async () => { + user.click(loginLink); + }); + expect(screen.queryByPlaceholderText('Email')).toBeNull(); + expect(screen.queryByPlaceholderText('Password')).toBeNull(); + expect(screen.queryByText('to continue to GraphiQL 🚀')).toBeNull(); + expect(await screen.findByText('Here is my fancy main page!')).toBeInTheDocument(); + }); +}); diff --git a/src/test/routing/authorizedMainRoute.test.tsx b/src/test/routing/authorizedMainRoute.test.tsx new file mode 100644 index 0000000..5f79ef1 --- /dev/null +++ b/src/test/routing/authorizedMainRoute.test.tsx @@ -0,0 +1,22 @@ +import { act, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; + +import App from '@/App'; +import { prepareAuthCookie } from '@/shared/helpers/cookieHandlers'; + +import userSetup from '../setupTests'; + +describe('Testing the authorized main page route', () => { + it('If user is authorized, and tries to react main route - he should be able to do so.', async () => { + document.cookie = prepareAuthCookie('test@gmail.com'); + const { user } = userSetup(); + const mainLink = await screen.findByText('main page'); + await act(async () => { + user.click(mainLink); + }); + expect(screen.queryByPlaceholderText('Email')).toBeNull(); + expect(screen.queryByPlaceholderText('Password')).toBeNull(); + expect(screen.queryByText('to continue to GraphiQL 🚀')).toBeNull(); + expect(await screen.findByText('Here is my fancy main page!')).toBeInTheDocument(); + }); +}); diff --git a/src/test/routing/unauthorizedLoginRoute.test.tsx b/src/test/routing/unauthorizedLoginRoute.test.tsx new file mode 100644 index 0000000..3e60a8b --- /dev/null +++ b/src/test/routing/unauthorizedLoginRoute.test.tsx @@ -0,0 +1,22 @@ +import { act, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; + +import App from '@/App'; + +import userSetup from '../setupTests'; + +describe('Testing the unauthorized login page route', () => { + it('If user is unauthorized, he should be able to visit login page.', async () => { + const { user } = userSetup(); + expect(screen.queryByPlaceholderText('Email')).toBeNull(); + expect(screen.queryByPlaceholderText('Password')).toBeNull(); + expect(screen.queryByText('to continue to GraphiQL 🚀')).toBeNull(); + const loginLink = await screen.findByText('login'); + await act(async () => { + user.click(loginLink); + }); + expect(await screen.findByPlaceholderText('Email')).toBeInTheDocument(); + expect(await screen.findByPlaceholderText('Password')).toBeInTheDocument(); + expect(await screen.findByText('to continue to GraphiQL 🚀')).toBeInTheDocument(); + }); +}); diff --git a/src/test/routing/unauthorizedMainRoute.test.tsx b/src/test/routing/unauthorizedMainRoute.test.tsx new file mode 100644 index 0000000..59d2502 --- /dev/null +++ b/src/test/routing/unauthorizedMainRoute.test.tsx @@ -0,0 +1,22 @@ +import { act, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; + +import App from '@/App'; + +import userSetup from '../setupTests'; + +describe('Testing the unauthorized main page route', () => { + it('If user is unauthorized, and tries to visit main page - he should be redirected to login page.', async () => { + const { user } = userSetup(); + expect(screen.queryByPlaceholderText('Email')).toBeNull(); + expect(screen.queryByPlaceholderText('Password')).toBeNull(); + expect(screen.queryByText('to continue to GraphiQL 🚀')).toBeNull(); + const mainLink = await screen.findByText('main page'); + await act(async () => { + user.click(mainLink); + }); + expect(await screen.findByPlaceholderText('Email')).toBeInTheDocument(); + expect(await screen.findByPlaceholderText('Password')).toBeInTheDocument(); + expect(await screen.findByText('to continue to GraphiQL 🚀')).toBeInTheDocument(); + }); +}); diff --git a/src/test/setupTests.ts b/src/test/setupTests.ts deleted file mode 100644 index f6c6efd..0000000 --- a/src/test/setupTests.ts +++ /dev/null @@ -1,25 +0,0 @@ -import * as matchers from '@testing-library/jest-dom/matchers'; -import { cleanup } from '@testing-library/react'; -import '@testing-library/jest-dom/vitest'; -import '@testing-library/jest-dom'; -import { afterEach, expect, vi } from 'vitest'; - -Object.defineProperty(window, 'matchMedia', { - writable: true, - value: vi.fn().mockImplementation((query) => ({ - matches: false, - media: query, - onchange: null, - addListener: vi.fn(), - removeListener: vi.fn(), - addEventListener: vi.fn(), - removeEventListener: vi.fn(), - dispatchEvent: vi.fn(), - })), -}); - -expect.extend(matchers); - -afterEach(() => { - cleanup(); -}); diff --git a/src/test/setupTests.tsx b/src/test/setupTests.tsx new file mode 100644 index 0000000..d0549d8 --- /dev/null +++ b/src/test/setupTests.tsx @@ -0,0 +1,57 @@ +import { PropsWithChildren } from 'react'; + +import * as matchers from '@testing-library/jest-dom/matchers'; +import { cleanup, render } from '@testing-library/react'; +import '@testing-library/jest-dom/vitest'; +import '@testing-library/jest-dom'; +import userEvent from '@testing-library/user-event'; +import { afterEach, expect, vi } from 'vitest'; + +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), +}); + +expect.extend(matchers); + +afterEach(() => { + cleanup(); + document.body.innerHTML = ''; +}); + +export default function userSetup(jsx: JSX.Element) { + return { + user: userEvent.setup(), + ...render(jsx), + }; +} + +vi.mock('@components/loginReg/SubmitBtn', () => ({ + default: () => , +})); + +vi.mock('@components/loginReg/FormInput', () => ({ + default: (props: PropsWithChildren) => ( + + ), +})); + +vi.mock('@components/loginReg/PassVisibilityIcon', () => ({ + default: () => , +})); + +vi.mock('firebase/auth', () => ({ + getAuth: () => {}, + signInWithEmailAndPassword: () => ({ user: { email: 'test@gmail.com' } }), +})); diff --git a/src/test/signUpPage.test.tsx b/src/test/signUpPage.test.tsx new file mode 100644 index 0000000..9412998 --- /dev/null +++ b/src/test/signUpPage.test.tsx @@ -0,0 +1,41 @@ +import { act, fireEvent, screen, waitFor } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; + +import App from '@/App'; + +import userSetup from './setupTests'; + +describe('Testing for sign up page', () => { + it('Should render sign up page properly', async () => { + const { user } = userSetup(); + const loginLink = await screen.findByText('login'); + await act(async () => { + user.click(loginLink); + }); + const singUpLink = await screen.findByText('Sign up'); + await act(async () => { + user.click(singUpLink); + }); + const emailInput = await screen.findByPlaceholderText('Email'); + const passInput = await screen.findByPlaceholderText('Password'); + const confPassInput = await screen.findByPlaceholderText('Confirm Password'); + await act(async () => { + fireEvent.change(emailInput, { + target: { value: 'asdrogachev@gmail.com' }, + }); + fireEvent.change(passInput, { + target: { value: '698830Pa$$' }, + }); + fireEvent.change(confPassInput, { + target: { value: '698830Pa$$' }, + }); + }); + const submit = await screen.findByRole('button', { name: 'Log in' }); + waitFor(async () => { + await user.click(submit); + }); + expect((emailInput as HTMLInputElement).value).toMatch('asdrogachev@gmail.com'); + expect((passInput as HTMLInputElement).value).toMatch('698830Pa$$'); + expect((confPassInput as HTMLInputElement).value).toMatch('698830Pa$$'); + }); +}); diff --git a/src/test/toastifyNotation.test.tsx b/src/test/toastifyNotation.test.tsx new file mode 100644 index 0000000..92dc7f2 --- /dev/null +++ b/src/test/toastifyNotation.test.tsx @@ -0,0 +1,13 @@ +import { render } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; + +import App from '@/App'; +import toastifyNotation from '@/shared/helpers/toastifyNotation'; + +describe('Testing for toastify notaions', () => { + it('Should display toastify alert with proper message', async () => { + render(); + toastifyNotation('Test msg'); + expect((document.body.firstChild as HTMLDivElement).innerText).toMatch('Test msg'); + }); +}); diff --git a/vite.config.ts b/vite.config.ts index 73cd2bc..d54cd04 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -21,14 +21,13 @@ export default defineConfig({ test: { globals: true, environment: 'jsdom', - setupFiles: './src/test/setupTests.ts', + setupFiles: './src/test/setupTests.tsx', css: false, coverage: { provider: 'istanbul', thresholds: { lines: 50, statements: 50, - branches: 50, functions: 50 }, }