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",