From 2c1a8ea127bd742f40ea70cb0883869d6775eae8 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Fri, 22 Aug 2025 05:28:56 +0300 Subject: [PATCH] feat(frontend): add authorization --- frontend/eslint.config.ts | 70 +++ frontend/package-lock.json | 448 +++++++++++++++++- frontend/package.json | 10 +- frontend/rsbuild.config.ts | 4 +- frontend/src/App.tsx | 7 - frontend/src/app/App/index.ts | 1 + frontend/src/app/App/ui/App.tsx | 36 ++ frontend/src/app/App/ui/normalize.css | 349 ++++++++++++++ frontend/src/entity/account/index.ts | 3 + frontend/src/entity/account/login/useLogin.ts | 37 ++ .../src/entity/account/logout/useLogout.ts | 30 ++ .../entity/account/register/useRegister.ts | 22 + .../src/features/auth/LogoutButton/index.ts | 1 + .../auth/LogoutButton/ui/LogoutButton.ui.tsx | 21 + frontend/src/index.tsx | 3 +- frontend/src/pages/Authorization/index.ts | 2 + .../ui/Login/LoginPage.module.css | 19 + .../Authorization/ui/Login/LoginPage.ui.tsx | 20 + .../ui/Register/RegisterPage.module.css | 19 + .../ui/Register/RegisterPage.ui.tsx | 21 + frontend/src/pages/Home/index.ts | 1 + frontend/src/pages/Home/ui/HomePage.ui.tsx | 51 ++ frontend/src/shared/api/client.ts | 46 ++ frontend/src/shared/api/schema.ts | 335 +++++++++++++ .../context/AuthContext/AuthContext.ui.tsx | 43 ++ .../src/shared/context/AuthContext/index.ts | 1 + frontend/src/shared/router/index.ts | 1 + frontend/src/shared/router/types.ts | 5 + frontend/src/shared/ui/AuthRoute/index.ts | 1 + .../shared/ui/AuthRoute/ui/AuthRoute.ui.tsx | 28 ++ frontend/src/shared/ui/input/index.ts | 1 + .../ui/input/ui/Input.styles.module.css | 4 + frontend/src/shared/ui/input/ui/Input.ui.tsx | 24 + frontend/src/shared/utils/index.ts | 3 + frontend/src/widgets/auth/LoginForm/index.ts | 1 + .../auth/LoginForm/ui/LoginForm.module.css | 10 + .../auth/LoginForm/ui/LoginForm.ui.tsx | 39 ++ .../auth/LoginForm/ui/hooks/useLoginForm.ts | 23 + .../src/widgets/auth/RegisterForm/index.ts | 1 + .../RegisterForm/ui/RegisterForm.module.css | 10 + .../auth/RegisterForm/ui/RegisterForm.ui.tsx | 36 ++ .../RegisterForm/ui/hooks/useRegisterForm.ts | 23 + frontend/tsconfig.json | 5 +- 43 files changed, 1788 insertions(+), 27 deletions(-) delete mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/app/App/index.ts create mode 100644 frontend/src/app/App/ui/App.tsx create mode 100644 frontend/src/app/App/ui/normalize.css create mode 100644 frontend/src/entity/account/index.ts create mode 100644 frontend/src/entity/account/login/useLogin.ts create mode 100644 frontend/src/entity/account/logout/useLogout.ts create mode 100644 frontend/src/entity/account/register/useRegister.ts create mode 100644 frontend/src/features/auth/LogoutButton/index.ts create mode 100644 frontend/src/features/auth/LogoutButton/ui/LogoutButton.ui.tsx create mode 100644 frontend/src/pages/Authorization/index.ts create mode 100644 frontend/src/pages/Authorization/ui/Login/LoginPage.module.css create mode 100644 frontend/src/pages/Authorization/ui/Login/LoginPage.ui.tsx create mode 100644 frontend/src/pages/Authorization/ui/Register/RegisterPage.module.css create mode 100644 frontend/src/pages/Authorization/ui/Register/RegisterPage.ui.tsx create mode 100644 frontend/src/pages/Home/index.ts create mode 100644 frontend/src/pages/Home/ui/HomePage.ui.tsx create mode 100644 frontend/src/shared/api/client.ts create mode 100644 frontend/src/shared/api/schema.ts create mode 100644 frontend/src/shared/context/AuthContext/AuthContext.ui.tsx create mode 100644 frontend/src/shared/context/AuthContext/index.ts create mode 100644 frontend/src/shared/router/index.ts create mode 100644 frontend/src/shared/router/types.ts create mode 100644 frontend/src/shared/ui/AuthRoute/index.ts create mode 100644 frontend/src/shared/ui/AuthRoute/ui/AuthRoute.ui.tsx create mode 100644 frontend/src/shared/ui/input/index.ts create mode 100644 frontend/src/shared/ui/input/ui/Input.styles.module.css create mode 100644 frontend/src/shared/ui/input/ui/Input.ui.tsx create mode 100644 frontend/src/shared/utils/index.ts create mode 100644 frontend/src/widgets/auth/LoginForm/index.ts create mode 100644 frontend/src/widgets/auth/LoginForm/ui/LoginForm.module.css create mode 100644 frontend/src/widgets/auth/LoginForm/ui/LoginForm.ui.tsx create mode 100644 frontend/src/widgets/auth/LoginForm/ui/hooks/useLoginForm.ts create mode 100644 frontend/src/widgets/auth/RegisterForm/index.ts create mode 100644 frontend/src/widgets/auth/RegisterForm/ui/RegisterForm.module.css create mode 100644 frontend/src/widgets/auth/RegisterForm/ui/RegisterForm.ui.tsx create mode 100644 frontend/src/widgets/auth/RegisterForm/ui/hooks/useRegisterForm.ts diff --git a/frontend/eslint.config.ts b/frontend/eslint.config.ts index 989ab00..5ed4703 100644 --- a/frontend/eslint.config.ts +++ b/frontend/eslint.config.ts @@ -1,7 +1,9 @@ +import pluginQuery from '@tanstack/eslint-plugin-query'; // @ts-expect-error fix package import config from 'eslint-config-ksv741'; export default [ + ...pluginQuery.configs['flat/recommended'], // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment ...config, { @@ -10,6 +12,74 @@ export default [ devDependencies: ['eslint.config.ts', 'rsbuild.config.ts'], }], 'import/no-anonymous-default-export': 'off', + 'import/order': [ + 'error', { + pathGroups: [ + { + pattern: '__spec__/**', + group: 'builtin', + position: 'before', + }, + { + pattern: 'apps/**', + group: 'internal', + position: 'after', + }, + { + pattern: 'pages/**', + group: 'internal', + position: 'after', + }, + { + pattern: 'widgets/**', + group: 'internal', + position: 'after', + }, + { + pattern: 'features/**', + group: 'internal', + position: 'after', + }, + { + pattern: 'entities/**', + group: 'internal', + position: 'after', + }, + { + pattern: 'shared/**', + group: 'internal', + position: 'after', + }, + ], + distinctGroup: true, + groups: [ + 'builtin', + 'external', + 'internal', + 'parent', + 'sibling', + 'object', + 'index', + 'type', + ], + 'newlines-between': 'always', + alphabetize: { + order: 'asc', + caseInsensitive: true, + }, + }, + ], + '@typescript-eslint/strict-boolean-expressions': 'off', + '@typescript-eslint/no-floating-promises': 'off', + '@typescript-eslint/consistent-return': 'off', + 'func-style': 'off', + '@typescript-eslint/no-confusing-void-expression': 'off', + '@typescript-eslint/await-thenable': 'off', + '@typescript-eslint/no-misused-promises': 'off', + '@typescript-eslint/prefer-nullish-coalescing': 'off', }, }, + { + ignores: ['./src/shared/api/schema.ts'], + }, ]; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d105d25..ede4c7f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,19 +8,24 @@ "name": "Investments Control Frontend", "version": "0.0.0", "dependencies": { + "@tanstack/react-query": "5.85.5", + "axios": "1.11.0", "react": "19.1.1", - "react-dom": "19.1.1" + "react-dom": "19.1.1", + "react-router": "7.8.1" }, "devDependencies": { "@rsbuild/core": "1.4.15", "@rsbuild/plugin-eslint": "1.1.2", "@rsbuild/plugin-react": "1.3.5", "@rsbuild/plugin-type-check": "1.2.4", + "@tanstack/eslint-plugin-query": "5.83.1", "@types/react": "19.1.9", "@types/react-dom": "19.1.7", "@typescript-eslint/parser": "8.39.0", "eslint": "9.33.0", "eslint-config-ksv741": "0.2.0", + "openapi-typescript": "7.9.1", "typescript": "5.9.2" } }, @@ -559,6 +564,82 @@ "node": ">= 8" } }, + "node_modules/@redocly/ajv": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.3.tgz", + "integrity": "sha512-4P3iZse91TkBiY+Dx5DUgxQ9GXkVJf++cmI0MOyLDxV9b5MUBI4II6ES8zA5JCbO72nKAJxWrw4PUPW+YP3ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js-replace": "^1.0.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@redocly/ajv/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/config": { + "version": "0.22.2", + "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.2.tgz", + "integrity": "sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/openapi-core": { + "version": "1.34.5", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.5.tgz", + "integrity": "sha512-0EbE8LRbkogtcCXU7liAyC00n9uNG9hJ+eMyHFdUsy9lB/WGqnEBgwjA9q2cyzAVcdTkQqTBBU1XePNnN3OijA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/ajv": "^8.11.2", + "@redocly/config": "^0.22.0", + "colorette": "^1.2.0", + "https-proxy-agent": "^7.0.5", + "js-levenshtein": "^1.1.6", + "js-yaml": "^4.1.0", + "minimatch": "^5.0.1", + "pluralize": "^8.0.0", + "yaml-ast-parser": "0.0.43" + }, + "engines": { + "node": ">=18.17.0", + "npm": ">=9.5.0" + } + }, + "node_modules/@redocly/openapi-core/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@redocly/openapi-core/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@rsbuild/core": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@rsbuild/core/-/core-1.4.15.tgz", @@ -903,6 +984,49 @@ "tslib": "^2.8.0" } }, + "node_modules/@tanstack/eslint-plugin-query": { + "version": "5.83.1", + "resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.83.1.tgz", + "integrity": "sha512-tdkpPFfzkTksN9BIlT/qjixSAtKrsW6PUVRwdKWaOcag7DrD1vpki3UzzdfMQGDRGeg1Ue1Dg+rcl5FJGembNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.37.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.85.5", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.85.5.tgz", + "integrity": "sha512-KO0WTob4JEApv69iYp1eGvfMSUkgw//IpMnq+//cORBzXf0smyRwPLrUvEe5qtAEGjwZTXrjxg+oJNP/C00t6w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.85.5", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.85.5.tgz", + "integrity": "sha512-/X4EFNcnPiSs8wM2v+b6DqS5mmGeuJQvxBglmDxl6ZQb5V26ouD2SJYAcC3VjbNwqhY2zjxVD15rDA5nGbMn3A==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.85.5" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", @@ -1531,6 +1655,16 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1590,6 +1724,16 @@ "dev": true, "license": "MIT" }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -1827,6 +1971,12 @@ "node": ">= 0.4" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -1853,6 +2003,17 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -1930,7 +2091,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -1984,6 +2144,13 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "dev": true, + "license": "MIT" + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -2058,6 +2225,25 @@ "dev": true, "license": "MIT" }, + "node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "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==", + "license": "MIT", + "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", @@ -2065,6 +2251,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/core-js": { "version": "3.45.0", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.0.tgz", @@ -2231,6 +2426,15 @@ "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==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -2248,7 +2452,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -2349,7 +2552,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2359,7 +2561,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2397,7 +2598,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -2410,7 +2610,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -3474,6 +3673,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -3490,6 +3709,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "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", @@ -3509,7 +3744,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3550,7 +3784,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -3575,7 +3808,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -3663,7 +3895,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3742,7 +3973,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3755,7 +3985,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -3771,7 +4000,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -3797,6 +4025,20 @@ ], "license": "MIT" }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/hyperdyperid": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", @@ -3844,6 +4086,19 @@ "node": ">=0.8.19" } }, + "node_modules/index-to-position": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.1.0.tgz", + "integrity": "sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -4391,6 +4646,16 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4545,7 +4810,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4615,6 +4879,27 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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", @@ -4801,6 +5086,40 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/openapi-typescript": { + "version": "7.9.1", + "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.9.1.tgz", + "integrity": "sha512-9gJtoY04mk6iPMbToPjPxEAtfXZ0dTsMZtsgUI8YZta0btPPig9DJFP4jlerQD/7QOwYgb0tl+zLUpDf7vb7VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/openapi-core": "^1.34.5", + "ansi-colors": "^4.1.3", + "change-case": "^5.4.4", + "parse-json": "^8.3.0", + "supports-color": "^10.1.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "openapi-typescript": "bin/cli.js" + }, + "peerDependencies": { + "typescript": "^5.x" + } + }, + "node_modules/openapi-typescript/node_modules/supports-color": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.0.tgz", + "integrity": "sha512-5eG9FQjEjDbAlI5+kdpdyPIBMRH4GfTVDGREVupaZHmVoppknhM29b/S9BkQz7cathp85BVgRi/As3Siln7e0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -4882,6 +5201,24 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4929,6 +5266,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -4961,6 +5308,12 @@ "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==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5030,6 +5383,28 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.1.tgz", + "integrity": "sha512-5cy/M8DHcG51/KUIka1nfZ2QeylS4PJRs6TT8I4PF5axVsI5JUxp0hC0NZ/AEEj8Vw7xsEoD7L/6FY+zoYaOGA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -5331,6 +5706,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -5836,6 +6217,19 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -5999,6 +6393,13 @@ "punycode": "^2.1.0" } }, + "node_modules/uri-js-replace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uri-js-replace/-/uri-js-replace-1.0.1.tgz", + "integrity": "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==", + "dev": true, + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6114,6 +6515,23 @@ "node": ">=0.10.0" } }, + "node_modules/yaml-ast-parser": { + "version": "0.0.43", + "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", + "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==", + "dev": true, + "license": "Apache-2.0" + }, + "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==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index c9cf2b0..3c1f8e6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,22 +8,28 @@ "dev": "rsbuild dev", "preview": "rsbuild preview", "lint": "eslint", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "openapi:gen": "npx openapi-typescript http://localhost:3000/docs/swagger.json -o src/shared/api/schema.ts" }, "dependencies": { + "@tanstack/react-query": "5.85.5", + "axios": "1.11.0", "react": "19.1.1", - "react-dom": "19.1.1" + "react-dom": "19.1.1", + "react-router": "7.8.1" }, "devDependencies": { "@rsbuild/core": "1.4.15", "@rsbuild/plugin-eslint": "1.1.2", "@rsbuild/plugin-react": "1.3.5", "@rsbuild/plugin-type-check": "1.2.4", + "@tanstack/eslint-plugin-query": "5.83.1", "@types/react": "19.1.9", "@types/react-dom": "19.1.7", "@typescript-eslint/parser": "8.39.0", "eslint": "9.33.0", "eslint-config-ksv741": "0.2.0", + "openapi-typescript": "7.9.1", "typescript": "5.9.2" } } diff --git a/frontend/rsbuild.config.ts b/frontend/rsbuild.config.ts index 0038531..0685c5a 100644 --- a/frontend/rsbuild.config.ts +++ b/frontend/rsbuild.config.ts @@ -1,6 +1,6 @@ import { defineConfig } from '@rsbuild/core'; -import { pluginReact } from '@rsbuild/plugin-react'; import { pluginEslint } from '@rsbuild/plugin-eslint'; +import { pluginReact } from '@rsbuild/plugin-react'; import { pluginTypeCheck } from '@rsbuild/plugin-type-check'; export default defineConfig({ @@ -9,6 +9,8 @@ export default defineConfig({ pluginEslint({ eslintPluginOptions: { configType: 'flat', + failOnError: false, + failOnWarning: false, }, }), pluginTypeCheck(), diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx deleted file mode 100644 index 68b9fcb..0000000 --- a/frontend/src/App.tsx +++ /dev/null @@ -1,7 +0,0 @@ -const App = () => ( -
-

Investments Control Frontend

-
-); - -export default App; diff --git a/frontend/src/app/App/index.ts b/frontend/src/app/App/index.ts new file mode 100644 index 0000000..22566d4 --- /dev/null +++ b/frontend/src/app/App/index.ts @@ -0,0 +1 @@ +export { default as App } from './ui/App'; diff --git a/frontend/src/app/App/ui/App.tsx b/frontend/src/app/App/ui/App.tsx new file mode 100644 index 0000000..d8f19c2 --- /dev/null +++ b/frontend/src/app/App/ui/App.tsx @@ -0,0 +1,36 @@ +import './normalize.css'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { + BrowserRouter, + Route, + Routes, +} from 'react-router'; + +import { LoginPage, RegisterPage } from '@/pages/Authorization'; +import { HomePage } from '@/pages/Home'; +import { AuthContextProvider } from '@/shared/context/AuthContext'; +import { AuthRoute } from '@/shared/ui/AuthRoute'; + +const queryClient = new QueryClient(); + +const App = () => ( + + + + + }> + } path="/" /> + + + }> + } path="/login" /> + + } path="/register" /> + + + + + +); + +export default App; diff --git a/frontend/src/app/App/ui/normalize.css b/frontend/src/app/App/ui/normalize.css new file mode 100644 index 0000000..192eb9c --- /dev/null +++ b/frontend/src/app/App/ui/normalize.css @@ -0,0 +1,349 @@ +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ + +/* Document + ========================================================================== */ + +/** + * 1. Correct the line height in all browsers. + * 2. Prevent adjustments of font size after orientation changes in iOS. + */ + +html { + line-height: 1.15; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} + +/* Sections + ========================================================================== */ + +/** + * Remove the margin in all browsers. + */ + +body { + margin: 0; +} + +/** + * Render the `main` element consistently in IE. + */ + +main { + display: block; +} + +/** + * Correct the font size and margin on `h1` elements within `section` and + * `article` contexts in Chrome, Firefox, and Safari. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/* Grouping content + ========================================================================== */ + +/** + * 1. Add the correct box sizing in Firefox. + * 2. Show the overflow in Edge and IE. + */ + +hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +pre { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/* Text-level semantics + ========================================================================== */ + +/** + * Remove the gray background on active links in IE 10. + */ + +a { + background-color: transparent; +} + +/** + * 1. Remove the bottom border in Chrome 57- + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. + */ + +abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ +} + +/** + * Add the correct font weight in Chrome, Edge, and Safari. + */ + +b, +strong { + font-weight: bolder; +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +code, +kbd, +samp { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/** + * Add the correct font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` elements from affecting the line height in + * all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Remove the border on images inside links in IE 10. + */ + +img { + border-style: none; +} + +/* Forms + ========================================================================== */ + +/** + * 1. Change the font styles in all browsers. + * 2. Remove the margin in Firefox and Safari. + */ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ +} + +/** + * Show the overflow in IE. + * 1. Show the overflow in Edge. + */ + +button, +input { /* 1 */ + overflow: visible; +} + +/** + * Remove the inheritance of text transform in Edge, Firefox, and IE. + * 1. Remove the inheritance of text transform in Firefox. + */ + +button, +select { /* 1 */ + text-transform: none; +} + +/** + * Correct the inability to style clickable types in iOS and Safari. + */ + +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; +} + +/** + * Remove the inner border and padding in Firefox. + */ + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; +} + +/** + * Restore the focus styles unset by the previous rule. + */ + +button:-moz-focusring, +[type="button"]:-moz-focusring, +[type="reset"]:-moz-focusring, +[type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; +} + +/** + * Correct the padding in Firefox. + */ + +fieldset { + padding: 0.35em 0.75em 0.625em; +} + +/** + * 1. Correct the text wrapping in Edge and IE. + * 2. Correct the color inheritance from `fieldset` elements in IE. + * 3. Remove the padding so developers are not caught out when they zero out + * `fieldset` elements in all browsers. + */ + +legend { + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ +} + +/** + * Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ + +progress { + vertical-align: baseline; +} + +/** + * Remove the default vertical scrollbar in IE 10+. + */ + +textarea { + overflow: auto; +} + +/** + * 1. Add the correct box sizing in IE 10. + * 2. Remove the padding in IE 10. + */ + +[type="checkbox"], +[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Correct the cursor style of increment and decrement buttons in Chrome. + */ + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ + +[type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +/** + * Remove the inner padding in Chrome and Safari on macOS. + */ + +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +/* Interactive + ========================================================================== */ + +/* + * Add the correct display in Edge, IE 10+, and Firefox. + */ + +details { + display: block; +} + +/* + * Add the correct display in all browsers. + */ + +summary { + display: list-item; +} + +/* Misc + ========================================================================== */ + +/** + * Add the correct display in IE 10+. + */ + +template { + display: none; +} + +/** + * Add the correct display in IE 10. + */ + +[hidden] { + display: none; +} diff --git a/frontend/src/entity/account/index.ts b/frontend/src/entity/account/index.ts new file mode 100644 index 0000000..859b060 --- /dev/null +++ b/frontend/src/entity/account/index.ts @@ -0,0 +1,3 @@ +export { useLogin } from './login/useLogin'; +export { useRegister } from './register/useRegister'; +export { useLogout } from './logout/useLogout'; diff --git a/frontend/src/entity/account/login/useLogin.ts b/frontend/src/entity/account/login/useLogin.ts new file mode 100644 index 0000000..5ffa304 --- /dev/null +++ b/frontend/src/entity/account/login/useLogin.ts @@ -0,0 +1,37 @@ +import { useMutation } from '@tanstack/react-query'; +import { useContext } from 'react'; +import { useNavigate } from 'react-router'; + +import { api } from '@/shared/api/client'; +import { AuthContext } from '@/shared/context/AuthContext'; + +import type { paths } from '@/shared/api/schema'; + +type LoginRequestBody = paths['/auth/login']['post']['requestBody']['content']['application/json']; +type LoginSuccessResponse = paths['/auth/login']['post']['responses']['200']['content']['application/json']; + +async function login(data: LoginRequestBody): Promise { + const { data: result } = await api.post('/auth/login', data); + + return result; +} + +export const useLogin = () => { + const navigate = useNavigate(); + const { setAccessToken } = useContext(AuthContext); + + return useMutation({ + mutationFn: login, + onSuccess: async (data) => { + setAccessToken(data.accessToken); + localStorage.setItem('accessToken', data.accessToken); + await navigate('/another'); + }, + onError: (error) => { + // eslint-disable-next-line + console.log('Неуспешный логин', error); + + throw error; + }, + }); +}; diff --git a/frontend/src/entity/account/logout/useLogout.ts b/frontend/src/entity/account/logout/useLogout.ts new file mode 100644 index 0000000..480d30d --- /dev/null +++ b/frontend/src/entity/account/logout/useLogout.ts @@ -0,0 +1,30 @@ +import { useMutation } from '@tanstack/react-query'; +import { useContext } from 'react'; +import { useNavigate } from 'react-router'; + +import { api } from '@/shared/api/client'; +import { AuthContext } from '@/shared/context/AuthContext'; + +import type { paths } from '@/shared/api/schema'; + +type LogoutSuccessResponse = paths['/auth/logout']['post']['responses']['200']['content']['application/json']; + +async function logout() { + const { data: result } = await api.post('/auth/logout'); + + return result; +} + +export const useLogout = () => { + const navigate = useNavigate(); + const { setAccessToken } = useContext(AuthContext); + + return useMutation({ + mutationFn: logout, + onSuccess: async () => { + setAccessToken(''); + localStorage.removeItem('accessToken'); + await navigate('/login'); + }, + }); +}; diff --git a/frontend/src/entity/account/register/useRegister.ts b/frontend/src/entity/account/register/useRegister.ts new file mode 100644 index 0000000..de5cdc6 --- /dev/null +++ b/frontend/src/entity/account/register/useRegister.ts @@ -0,0 +1,22 @@ +import { useMutation } from '@tanstack/react-query'; + +import { api } from '@/shared/api/client'; + +import type { paths } from '@/shared/api/schema'; + +type RegisterRequestBody = paths['/auth/register']['post']['requestBody']['content']['application/json']; +type RegisterSuccessResponse = paths['/auth/register']['post']['responses']['200']['content']['application/json']; + +async function register(data: RegisterRequestBody) { + const { data: result } = await api.post('auth/register', data); + + return result; +} + +export const useRegister = () => useMutation({ + mutationFn: register, + onError: (error) => { + // eslint-disable-next-line no-console + console.log('Неуспешная регистрация', error); + }, +}); diff --git a/frontend/src/features/auth/LogoutButton/index.ts b/frontend/src/features/auth/LogoutButton/index.ts new file mode 100644 index 0000000..77f6010 --- /dev/null +++ b/frontend/src/features/auth/LogoutButton/index.ts @@ -0,0 +1 @@ +export { default as LogoutButton } from './ui/LogoutButton.ui'; diff --git a/frontend/src/features/auth/LogoutButton/ui/LogoutButton.ui.tsx b/frontend/src/features/auth/LogoutButton/ui/LogoutButton.ui.tsx new file mode 100644 index 0000000..341452a --- /dev/null +++ b/frontend/src/features/auth/LogoutButton/ui/LogoutButton.ui.tsx @@ -0,0 +1,21 @@ +import { useLogout } from '@/entity/account'; + +const LogoutButton = () => { + const { mutate: logout, isPending } = useLogout(); + + const onClickHandler = async () => { + await logout(); + }; + + return ( + + ); +}; + +export default LogoutButton; diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 55f29bf..e31917a 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import App from './App'; + +import { App } from '@/app/App'; const rootEl = document.getElementById('root'); if (rootEl) { diff --git a/frontend/src/pages/Authorization/index.ts b/frontend/src/pages/Authorization/index.ts new file mode 100644 index 0000000..b419c5c --- /dev/null +++ b/frontend/src/pages/Authorization/index.ts @@ -0,0 +1,2 @@ +export { default as LoginPage } from './ui/Login/LoginPage.ui'; +export { default as RegisterPage } from './ui/Register/RegisterPage.ui'; diff --git a/frontend/src/pages/Authorization/ui/Login/LoginPage.module.css b/frontend/src/pages/Authorization/ui/Login/LoginPage.module.css new file mode 100644 index 0000000..a177e78 --- /dev/null +++ b/frontend/src/pages/Authorization/ui/Login/LoginPage.module.css @@ -0,0 +1,19 @@ +.overlay { + height: 100vh; + width: 100vw; + display: flex; + justify-content: center; + align-items: center; + background: darkgrey; +} + +.contentWrapper { + width: 400px; + height: 500px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + border-radius: 10px; + background: lightgray; +} diff --git a/frontend/src/pages/Authorization/ui/Login/LoginPage.ui.tsx b/frontend/src/pages/Authorization/ui/Login/LoginPage.ui.tsx new file mode 100644 index 0000000..8204f03 --- /dev/null +++ b/frontend/src/pages/Authorization/ui/Login/LoginPage.ui.tsx @@ -0,0 +1,20 @@ +import { NavLink } from 'react-router'; + +import { LoginForm } from '@/widgets/auth/LoginForm'; + +import classes from './LoginPage.module.css'; + +const LoginPage = () => ( +
+
+

Вход в аккаунт

+ + + + + Регистрация + +
+
+); +export default LoginPage; diff --git a/frontend/src/pages/Authorization/ui/Register/RegisterPage.module.css b/frontend/src/pages/Authorization/ui/Register/RegisterPage.module.css new file mode 100644 index 0000000..a177e78 --- /dev/null +++ b/frontend/src/pages/Authorization/ui/Register/RegisterPage.module.css @@ -0,0 +1,19 @@ +.overlay { + height: 100vh; + width: 100vw; + display: flex; + justify-content: center; + align-items: center; + background: darkgrey; +} + +.contentWrapper { + width: 400px; + height: 500px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + border-radius: 10px; + background: lightgray; +} diff --git a/frontend/src/pages/Authorization/ui/Register/RegisterPage.ui.tsx b/frontend/src/pages/Authorization/ui/Register/RegisterPage.ui.tsx new file mode 100644 index 0000000..b424ca5 --- /dev/null +++ b/frontend/src/pages/Authorization/ui/Register/RegisterPage.ui.tsx @@ -0,0 +1,21 @@ +import { NavLink } from 'react-router'; + +import { RegisterForm } from '@/widgets/auth/RegisterForm'; + +import classes from './RegisterPage.module.css'; + +const RegisterPage = () => ( +
+
+

Регистрация аккаунта

+ + + + + Вход + +
+
+); + +export default RegisterPage; diff --git a/frontend/src/pages/Home/index.ts b/frontend/src/pages/Home/index.ts new file mode 100644 index 0000000..dcfa855 --- /dev/null +++ b/frontend/src/pages/Home/index.ts @@ -0,0 +1 @@ +export { default as HomePage } from './ui/HomePage.ui'; diff --git a/frontend/src/pages/Home/ui/HomePage.ui.tsx b/frontend/src/pages/Home/ui/HomePage.ui.tsx new file mode 100644 index 0000000..397cf1a --- /dev/null +++ b/frontend/src/pages/Home/ui/HomePage.ui.tsx @@ -0,0 +1,51 @@ +import { useContext, useState } from 'react'; + +import { api } from '@/shared/api/client'; +import { AuthContext } from '@/shared/context/AuthContext'; + +import type { paths } from '@/shared/api/schema'; + +type Response = paths['/auth/me']['get']['responses']['200']['content']['application/json']; + +const fetchProfile = async () => { + const { data } = await api.get('/auth/me'); + + return data; +}; + +const HomePage = () => { + const [info, setInfo] = useState(); + const { accessToken } = useContext(AuthContext); + + const onClick = () => { + if (!accessToken) { + return; + } + + fetchProfile() + .then((data) => { + // eslint-disable-next-line no-console + console.log('DATA', data); + + setInfo(data); + }); + }; + + return ( +
+ Home page only for auth user + + +
+ + + ID: + {info?.id} + +
+ ); +}; + +export default HomePage; diff --git a/frontend/src/shared/api/client.ts b/frontend/src/shared/api/client.ts new file mode 100644 index 0000000..3468243 --- /dev/null +++ b/frontend/src/shared/api/client.ts @@ -0,0 +1,46 @@ +import axios from 'axios'; + +import type { paths } from './schema'; + +const BASE_URL = 'http://localhost:3000'; + +export const api = axios.create({ + baseURL: BASE_URL, + withCredentials: true, +}); + +api.interceptors.request.use( + (request) => { + const accessToken = localStorage.getItem('accessToken'); + if (accessToken) { + request.headers.set('Authorization', `Bearer ${accessToken}`); + } + + return request; + }, + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + async (error) => Promise.reject(error), +); + +type RefreshTokensSuccessResponse = paths['/auth/refresh']['post']['responses']['200']['content']['application/json']; + +api.interceptors.response.use((response) => response, async (error) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access + const originalRequest = error.config; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (error.response?.status === 401 && !originalRequest._retry) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + originalRequest._retry = true; + const { data: result } = await axios.post(`${BASE_URL}/auth/refresh`, null, { + withCredentials: true, + }); + const { accessToken } = result; + localStorage.setItem('accessToken', accessToken); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access + return api(error.config); + } + + throw error; +}); diff --git a/frontend/src/shared/api/schema.ts b/frontend/src/shared/api/schema.ts new file mode 100644 index 0000000..508effe --- /dev/null +++ b/frontend/src/shared/api/schema.ts @@ -0,0 +1,335 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/auth/register": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Регистрация + * @description Регистрация аккаунта + */ + post: operations["AuthController_register"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/login": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Логин + * @description Вход в аккаунт + */ + post: operations["AuthController_login"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/logout": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Логаут + * @description Выход из аккаунта + */ + post: operations["AuthController_logout"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Получить профиль + * @description Получить данные о своем профиле + */ + get: operations["AuthController_me"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/refresh": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Обновление access и refresh токенов */ + post: operations["AuthController_refresh"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + RegisterDto: { + /** + * @description Email + * @example mail@example.com + */ + email: string; + /** + * @description Пароль + * @example 123456 + */ + password: string; + }; + RegisterValidationDto: { + /** @example 400 */ + statusCode: number; + /** @example [ + * "Email должен быть строкой.", + * "Некорректный формат email.", + * "Email обязателен для заполнения.", + * "Пароль должен быть строкой.", + * "Пароль обязателен для заполнения.", + * "Пароль должен содержать минимум 6 символов." + * ] */ + message: string[]; + /** @example Bad Request */ + error: string; + }; + ConflictRegisterDto: { + /** @example 400 */ + statusCode: number; + /** @example Пользователь с такой почтой уже зарегистрирован */ + message: Record; + /** @example Conflict */ + error: string; + }; + LoginDto: { + /** + * @description Email + * @example mail@example.com + */ + email: string; + /** + * @description Пароль + * @example 123456 + */ + password: string; + }; + SuccessLoginDto: { + /** @description Access токен */ + accessToken: string; + }; + IncorrectLoginDto: { + /** @example 400 */ + statusCode: number; + /** @example Неверный логин или пароль */ + message: Record; + /** @example Bad Request */ + error: string; + }; + CurrentProfileDto: { + /** @description Индетификатор аккаунта */ + id: string; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + AuthController_register: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["RegisterDto"]; + }; + }; + responses: { + /** @description Успешная регистрация */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RegisterDto"]; + }; + }; + /** @description Ошибка валидации */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RegisterValidationDto"]; + }; + }; + /** @description Ошибка создания аккаунта */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ConflictRegisterDto"]; + }; + }; + }; + }; + AuthController_login: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["LoginDto"]; + }; + }; + responses: { + /** @description Успешный вход в аккаунт */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SuccessLoginDto"]; + }; + }; + /** @description Неверный логин или пароль */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["IncorrectLoginDto"]; + }; + }; + }; + }; + AuthController_logout: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Успешный выход из аккаунт */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": boolean; + }; + }; + }; + }; + AuthController_me: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Успешное обновление токенов */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CurrentProfileDto"]; + }; + }; + /** @description Пользователь не авторизован */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + AuthController_refresh: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Успешное обновление токенов */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SuccessLoginDto"]; + }; + }; + /** @description Пользователь не авторизован */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; +} diff --git a/frontend/src/shared/context/AuthContext/AuthContext.ui.tsx b/frontend/src/shared/context/AuthContext/AuthContext.ui.tsx new file mode 100644 index 0000000..c9ef3b4 --- /dev/null +++ b/frontend/src/shared/context/AuthContext/AuthContext.ui.tsx @@ -0,0 +1,43 @@ +import { + createContext, + useMemo, + useState, +} from 'react'; + +import type { + Dispatch, + FC, + ReactNode, + SetStateAction, +} from 'react'; + +export type AuthContextType = { + accessToken: string; + setAccessToken: Dispatch>; +}; +// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion +export const AuthContext = createContext(null as unknown as AuthContextType); + +function getInitialToken() { + return localStorage.getItem('accessToken') ?? ''; +} + +type AuthContextProviderProps = { + children: ReactNode; +}; +export const AuthContextProvider: FC = (props) => { + const { children } = props; + + const [accessToken, setAccessToken] = useState(getInitialToken); + + const value = useMemo(() => ({ + accessToken, + setAccessToken, + }), [accessToken]); + + return ( + + {children} + + ); +}; diff --git a/frontend/src/shared/context/AuthContext/index.ts b/frontend/src/shared/context/AuthContext/index.ts new file mode 100644 index 0000000..4a3f544 --- /dev/null +++ b/frontend/src/shared/context/AuthContext/index.ts @@ -0,0 +1 @@ +export { AuthContext, AuthContextProvider } from './AuthContext.ui'; diff --git a/frontend/src/shared/router/index.ts b/frontend/src/shared/router/index.ts new file mode 100644 index 0000000..c814d5b --- /dev/null +++ b/frontend/src/shared/router/index.ts @@ -0,0 +1 @@ +export { PAGES } from './types'; diff --git a/frontend/src/shared/router/types.ts b/frontend/src/shared/router/types.ts new file mode 100644 index 0000000..51463c9 --- /dev/null +++ b/frontend/src/shared/router/types.ts @@ -0,0 +1,5 @@ +export enum PAGES { + Home = '/', + Login = '/login', + Register = '/register', +} diff --git a/frontend/src/shared/ui/AuthRoute/index.ts b/frontend/src/shared/ui/AuthRoute/index.ts new file mode 100644 index 0000000..f618c39 --- /dev/null +++ b/frontend/src/shared/ui/AuthRoute/index.ts @@ -0,0 +1 @@ +export { default as AuthRoute } from './ui/AuthRoute.ui'; diff --git a/frontend/src/shared/ui/AuthRoute/ui/AuthRoute.ui.tsx b/frontend/src/shared/ui/AuthRoute/ui/AuthRoute.ui.tsx new file mode 100644 index 0000000..148cc49 --- /dev/null +++ b/frontend/src/shared/ui/AuthRoute/ui/AuthRoute.ui.tsx @@ -0,0 +1,28 @@ +import { type FC, useContext } from 'react'; +import { Navigate, Outlet } from 'react-router'; + +import { AuthContext } from '@/shared/context/AuthContext'; + +import type { NavigateProps } from 'react-router'; + +type AuthRouteProps = { + needAuth?: boolean; + to?: NavigateProps['to']; +}; +const AuthRoute: FC = (props) => { + const { + needAuth = true, + to = '/login', + } = props; + const { accessToken } = useContext(AuthContext); + + if ((needAuth && accessToken) || (!needAuth && !accessToken)) { + return ; + } + + return ( + + ); +}; + +export default AuthRoute; diff --git a/frontend/src/shared/ui/input/index.ts b/frontend/src/shared/ui/input/index.ts new file mode 100644 index 0000000..578116d --- /dev/null +++ b/frontend/src/shared/ui/input/index.ts @@ -0,0 +1 @@ +export { default as Input } from './ui/Input.ui'; diff --git a/frontend/src/shared/ui/input/ui/Input.styles.module.css b/frontend/src/shared/ui/input/ui/Input.styles.module.css new file mode 100644 index 0000000..d6da646 --- /dev/null +++ b/frontend/src/shared/ui/input/ui/Input.styles.module.css @@ -0,0 +1,4 @@ +.wrapper { + display: flex; + flex-direction: column; +} diff --git a/frontend/src/shared/ui/input/ui/Input.ui.tsx b/frontend/src/shared/ui/input/ui/Input.ui.tsx new file mode 100644 index 0000000..25b0cde --- /dev/null +++ b/frontend/src/shared/ui/input/ui/Input.ui.tsx @@ -0,0 +1,24 @@ +import classes from './Input.styles.module.css'; + +import type { FC, HTMLProps } from 'react'; + +type InputProps = HTMLProps & { + label: string; +}; + +const Input: FC = (props) => { + const { label, id, ...rest } = props; + + return ( + + ); +}; + +export default Input; diff --git a/frontend/src/shared/utils/index.ts b/frontend/src/shared/utils/index.ts new file mode 100644 index 0000000..34cb8b8 --- /dev/null +++ b/frontend/src/shared/utils/index.ts @@ -0,0 +1,3 @@ +export const wait = async (time: number) => new Promise((res) => { + setTimeout(res, time); +}); diff --git a/frontend/src/widgets/auth/LoginForm/index.ts b/frontend/src/widgets/auth/LoginForm/index.ts new file mode 100644 index 0000000..84c2fb2 --- /dev/null +++ b/frontend/src/widgets/auth/LoginForm/index.ts @@ -0,0 +1 @@ +export { default as LoginForm } from './ui/LoginForm.ui'; diff --git a/frontend/src/widgets/auth/LoginForm/ui/LoginForm.module.css b/frontend/src/widgets/auth/LoginForm/ui/LoginForm.module.css new file mode 100644 index 0000000..71b8b2f --- /dev/null +++ b/frontend/src/widgets/auth/LoginForm/ui/LoginForm.module.css @@ -0,0 +1,10 @@ +.form { + display: flex; + flex-direction: column; + gap: 10px; + width: 300px; + + button:disabled { + background: red; + } +} diff --git a/frontend/src/widgets/auth/LoginForm/ui/LoginForm.ui.tsx b/frontend/src/widgets/auth/LoginForm/ui/LoginForm.ui.tsx new file mode 100644 index 0000000..b43473d --- /dev/null +++ b/frontend/src/widgets/auth/LoginForm/ui/LoginForm.ui.tsx @@ -0,0 +1,39 @@ +import { Input } from '@/shared/ui/input'; + +import { useLoginForm } from './hooks/useLoginForm'; +import classes from './LoginForm.module.css'; + +import type { FC } from 'react'; + +const LoginForm: FC = () => { + const { isPending, onSubmitHandler } = useLoginForm(); + + return ( +
+ + + + + +
+ ); +}; + +export default LoginForm; diff --git a/frontend/src/widgets/auth/LoginForm/ui/hooks/useLoginForm.ts b/frontend/src/widgets/auth/LoginForm/ui/hooks/useLoginForm.ts new file mode 100644 index 0000000..c96b5a4 --- /dev/null +++ b/frontend/src/widgets/auth/LoginForm/ui/hooks/useLoginForm.ts @@ -0,0 +1,23 @@ +import { useLogin } from '@/entity/account'; + +import type { FormEventHandler } from 'react'; + +export const useLoginForm = () => { + const { mutate: login, isPending } = useLogin(); + + const onSubmitHandler: FormEventHandler = (event) => { + event.preventDefault(); + const formData = new FormData(event.currentTarget); + const email = formData.get('email'); + const password = formData.get('password'); + + if (typeof email === 'string' && typeof password === 'string') { + login({ email, password }); + } + }; + + return { + isPending, + onSubmitHandler, + }; +}; diff --git a/frontend/src/widgets/auth/RegisterForm/index.ts b/frontend/src/widgets/auth/RegisterForm/index.ts new file mode 100644 index 0000000..a578823 --- /dev/null +++ b/frontend/src/widgets/auth/RegisterForm/index.ts @@ -0,0 +1 @@ +export { default as RegisterForm } from './ui/RegisterForm.ui'; diff --git a/frontend/src/widgets/auth/RegisterForm/ui/RegisterForm.module.css b/frontend/src/widgets/auth/RegisterForm/ui/RegisterForm.module.css new file mode 100644 index 0000000..71b8b2f --- /dev/null +++ b/frontend/src/widgets/auth/RegisterForm/ui/RegisterForm.module.css @@ -0,0 +1,10 @@ +.form { + display: flex; + flex-direction: column; + gap: 10px; + width: 300px; + + button:disabled { + background: red; + } +} diff --git a/frontend/src/widgets/auth/RegisterForm/ui/RegisterForm.ui.tsx b/frontend/src/widgets/auth/RegisterForm/ui/RegisterForm.ui.tsx new file mode 100644 index 0000000..78eae8a --- /dev/null +++ b/frontend/src/widgets/auth/RegisterForm/ui/RegisterForm.ui.tsx @@ -0,0 +1,36 @@ +import { Input } from '@/shared/ui/input'; + +import { useRegisterForm } from './hooks/useRegisterForm'; +import classes from './RegisterForm.module.css'; + +import type { FC } from 'react'; + +const RegisterForm: FC = () => { + const { onSubmitHandler, isPending } = useRegisterForm(); + + return ( +
+ + + + + +
+ ); +}; + +export default RegisterForm; diff --git a/frontend/src/widgets/auth/RegisterForm/ui/hooks/useRegisterForm.ts b/frontend/src/widgets/auth/RegisterForm/ui/hooks/useRegisterForm.ts new file mode 100644 index 0000000..a6f8a69 --- /dev/null +++ b/frontend/src/widgets/auth/RegisterForm/ui/hooks/useRegisterForm.ts @@ -0,0 +1,23 @@ +import { useRegister } from '@/entity/account'; + +import type { FormEventHandler } from 'react'; + +export const useRegisterForm = () => { + const { mutate: register, isPending } = useRegister(); + + const onSubmitHandler: FormEventHandler = (event) => { + event.preventDefault(); + const formData = new FormData(event.currentTarget); + const email = formData.get('email'); + const password = formData.get('password'); + + if (typeof email === 'string' && typeof password === 'string') { + register({ email, password }); + } + }; + + return { + onSubmitHandler, + isPending, + }; +}; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 116e039..b840a24 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -19,7 +19,10 @@ /* type checking */ "strict": true, "noUnusedLocals": true, - "noUnusedParameters": true + "noUnusedParameters": true, + "paths": { + "@/*": ["./src/*"] + } }, "include": [ "src",