diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000000000000000000000000000000000000..6da56594416abe2a8b99759f62b2d426ffc12fb6
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,22 @@
+# Файл с настройками для редактора.
+#
+# Если вы разрабатываете в редакторе WebStorm, BBEdit, Coda или SourceLair
+# этот файл уже поддерживается и не нужно производить никаких дополнительных
+# действий.
+#
+# Если вы ведёте разработку в другом редакторе, зайдите
+# на https://editorconfig.org и в разделе «Download a Plugin»
+# скачайте дополнение для вашего редактора.
+
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 4
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.{json,md}]
+indent_size = 2
diff --git a/.eslintrc.cjs b/.eslintrc.cjs
new file mode 100644
index 0000000000000000000000000000000000000000..05a6ff8c3f5165ff5e6cf90a97a09ec67699ea90
--- /dev/null
+++ b/.eslintrc.cjs
@@ -0,0 +1,55 @@
+module.exports = {
+ "env": {
+ "browser": true,
+ "es2021": true
+ },
+ "extends": [
+ 'plugin:react/recommended',
+ 'plugin:@tanstack/eslint-plugin-query/recommended',
+ 'plugin:storybook/recommended',
+ 'airbnb',
+ 'plugin:prettier/recommended'
+ ],
+ "overrides": [
+ {
+ "env": {
+ "node": true
+ },
+ "files": [
+ ".eslintrc.{js,cjs}"
+ ],
+ "parserOptions": {
+ "sourceType": "script"
+ }
+ }
+ ],
+ "parser": "@typescript-eslint/parser",
+ "parserOptions": {
+ "ecmaVersion": "latest",
+ "sourceType": "module"
+ },
+ "plugins": [
+ "@typescript-eslint",
+ "react",
+ "@tanstack/query"
+ ],
+ "ignorePatterns": ["*.svg", "*.json"],
+ "rules": {
+ "react/jsx-filename-extension": ["error", { "extensions": [".tsx"] }],
+ 'import/no-unresolved': 'off',
+ 'import/prefer-default-export': 'off',
+ 'no-unused-vars': 'warn',
+ 'react/require-default-props': 'off',
+ 'react/react-in-jsx-scope': 'off',
+ 'react/jsx-props-no-spreading': 'warn',
+ 'react/function-component-definition': 'off',
+ 'no-shadow': 'off',
+ 'import/extensions': 'off',
+ 'import/no-extraneous-dependencies': 'off',
+ 'no-underscore-dangle': 'off',
+ 'no-param-reassign': 'off',
+ 'no-undef': 'off',
+ "react/jsx-max-props-per-line": ['error', {maximum: 3}],
+ "prettier/prettier": ['error', {arrowParens: 'always'}]
+ }
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..063c78f0fd1fa1754ea84d49e5f35204f856ad08
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,27 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+build
+package-lock.json
+dist-ssr
+*.local
+.env
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000000000000000000000000000000000000..233dabe4b032c2ac40aa36f75c50cd210288c091
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,12 @@
+{
+ "printWidth": 130,
+ "semi": true,
+ "singleQuote": true,
+ "tabWidth": 4,
+ "jsxSingleQuote": false,
+ "bracketSpacing": true,
+ "trailingComma": "es5",
+ "arrowParens": "always",
+ "jsxBracketSameLine": false,
+ "endOfLine": "lf"
+}
diff --git a/.storybook/main.ts b/.storybook/main.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bedd614fb76b94c6d95e6cd7fff52d70de6625c4
--- /dev/null
+++ b/.storybook/main.ts
@@ -0,0 +1,25 @@
+import type { StorybookConfig } from '@storybook/react-vite';
+import svgr from 'vite-plugin-svgr';
+
+const config: StorybookConfig = {
+ stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
+ addons: [
+ '@storybook/addon-links',
+ '@storybook/addon-essentials',
+ '@storybook/addon-onboarding',
+ '@storybook/addon-interactions',
+ ],
+ framework: {
+ name: '@storybook/react-vite',
+ options: {},
+ },
+ docs: {
+ autodocs: 'tag',
+ },
+ viteFinal: async (config) => {
+ config.plugins.push(svgr());
+
+ return config;
+ },
+};
+export default config;
diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..bc5aa40e95cf36985a4ff098e83d8fa478ce0933
--- /dev/null
+++ b/.storybook/preview.tsx
@@ -0,0 +1,45 @@
+import type { Preview } from '@storybook/react';
+import { QueryProvider } from '../src/app/providers/QueryProvider';
+import ThemeProvider from '../src/app/providers/ThemeProviders/ui/ThemeProvider';
+import { useTheme } from '../src/app/providers/ThemeProviders';
+import '../src/app/globalStyles/styles.scss';
+
+const preview: Preview = {
+ parameters: {
+ actions: { argTypesRegex: '^on[A-Z].*' },
+ controls: {
+ matchers: {
+ color: /(background|color)$/i,
+ date: /Date$/i,
+ },
+ },
+ },
+ globalTypes: {
+ theme: {
+ description: 'Выбрать тему',
+ defaultValue: 'light',
+ toolbar: {
+ title: 'Theme',
+ icon: 'circlehollow',
+ items: ['light', 'dark'],
+ dynamicTitle: true,
+ },
+ },
+ },
+ decorators: [
+ (Story, context) => {
+ const { toggleTheme } = useTheme();
+ toggleTheme(context.globals.theme);
+
+ return (
+
+
+
+
+
+ );
+ },
+ ],
+};
+
+export default preview;
diff --git a/index.html b/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..bdd5ea6e2e2acebc8ed1aeda3a687bb60e868b43
--- /dev/null
+++ b/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Добро пожаловать!
+
+
+
+
+
+
diff --git a/package.json b/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..7cb14fae7877c25bba5974452827bd523d33ee65
--- /dev/null
+++ b/package.json
@@ -0,0 +1,71 @@
+{
+ "name": "webrise-vite-start",
+ "description": "node 20.11.0",
+ "private": true,
+ "version": "0.0.0",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
+ "preview": "vite preview",
+ "storybook": "storybook dev -p 6006",
+ "build-storybook": "storybook build",
+ "plop:entity": "plop entity --plopfile ./plop/plopfile.js",
+ "plop:feature": "plop feature --plopfile ./plop/plopfile.js",
+ "plop:page": "plop page --plopfile ./plop/plopfile.js",
+ "plop:shared": "plop shared --plopfile ./plop/plopfile.js",
+ "plop:widget": "plop widget --plopfile ./plop/plopfile.js",
+ "plop:api": "plop api --plopfile ./plop/plopfile.js",
+ "plop:form": "plop form --plopfile ./plop/plopfile.js",
+ "plop:store": "plop store --plopfile ./plop/plopfile.js",
+ "plop:slice": "plop slice --plopfile ./plop/plopfile.js",
+ "plop:component": "plop component --plopfile ./plop/plopfile.js"
+ },
+ "dependencies": {
+ "@hookform/resolvers": "^3.3.4",
+ "@tanstack/react-query": "^5.18.1",
+ "@tanstack/react-query-devtools": "^5.18.1",
+ "axios": "^1.6.7",
+ "inputmask": "^5.0.8",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-hook-form": "^7.50.0",
+ "react-router-dom": "^6.22.0",
+ "zod": "^3.22.4",
+ "zustand": "^4.5.0"
+ },
+ "devDependencies": {
+ "@storybook/addon-essentials": "^7.6.14",
+ "@storybook/addon-interactions": "^7.6.14",
+ "@storybook/addon-links": "^7.6.14",
+ "@storybook/addon-onboarding": "^1.0.11",
+ "@storybook/blocks": "^7.6.14",
+ "@storybook/builder-vite": "^7.6.14",
+ "@storybook/react": "^7.6.14",
+ "@storybook/react-vite": "^7.6.14",
+ "@storybook/test": "^7.6.14",
+ "@tanstack/eslint-plugin-query": "^5.18.1",
+ "@types/react": "^18.2.43",
+ "@types/react-dom": "^18.2.17",
+ "@typescript-eslint/eslint-plugin": "^6.19.1",
+ "@typescript-eslint/parser": "^6.19.1",
+ "@vitejs/plugin-react": "^4.2.1",
+ "eslint": "^8.56.0",
+ "eslint-config-airbnb": "^19.0.4",
+ "eslint-config-prettier": "^9.1.0",
+ "eslint-plugin-import": "^2.29.1",
+ "eslint-plugin-jsx-a11y": "^6.8.0",
+ "eslint-plugin-prettier": "^5.1.3",
+ "eslint-plugin-react": "^7.33.2",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "eslint-plugin-react-refresh": "^0.4.5",
+ "eslint-plugin-storybook": "^0.6.15",
+ "plop": "^4.0.1",
+ "prettier": "^3.2.4",
+ "sass": "^1.70.0",
+ "storybook": "^7.6.14",
+ "typescript": "^5.2.2",
+ "vite": "^5.0.8",
+ "vite-plugin-svgr": "^4.2.0"
+ }
+}
diff --git a/plop/generation/common/api.js b/plop/generation/common/api.js
new file mode 100644
index 0000000000000000000000000000000000000000..b822bffe4d0941070b7347cf1e7fbe60ec3a2ab0
--- /dev/null
+++ b/plop/generation/common/api.js
@@ -0,0 +1,141 @@
+const api = (plop) => {
+ plop.setGenerator('api', {
+ description: 'Создает апи',
+ prompts: [
+ {
+ type: 'list',
+ name: 'apiType',
+ message: 'Какой тип запроса?',
+ choices: [
+ {
+ name: 'Запрос списка (fetchNames)',
+ value: 'fetchList',
+ },
+ {
+ name: 'Запрос по id (fetchNameById)',
+ value: 'fetchById',
+ },
+ {
+ name: 'Создание слайса (createName)',
+ value: 'create',
+ },
+ {
+ name: 'Обновление слайса (updateName)',
+ value: 'update',
+ },
+ {
+ name: 'Удаление слайса (deleteName)',
+ value: 'delete',
+ },
+ ],
+ },
+ {
+ type: 'list',
+ name: 'layerName',
+ message: 'В какой слой положить?',
+ choices: [
+ {
+ name: 'entities',
+ value: 'entities',
+ },
+ {
+ name: 'features',
+ value: 'features',
+ },
+ {
+ name: 'widgets',
+ value: 'widgets',
+ },
+ ],
+ },
+ {
+ type: 'input',
+ name: 'sliceName',
+ message: 'В какой слайс?',
+ },
+ ],
+ actions: (data) => {
+ let actionList = [];
+
+ switch (data.apiType) {
+ case 'fetchList':
+ actionList = [
+ {
+ type: 'add',
+ path: '../src/{{layerName}}/{{sliceName}}/api/fetch{{sliceName}}s.ts',
+ templateFile: './templates/api/fetchApi.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/{{layerName}}/{{sliceName}}/lib/query/useFetch{{sliceName}}s.tsx',
+ templateFile: './templates/query/fetchQuery.hbs',
+ },
+ ];
+ break;
+ case 'fetchById':
+ actionList = [
+ {
+ type: 'add',
+ path: '../src/{{layerName}}/{{sliceName}}/api/fetch{{sliceName}}ById.ts',
+ templateFile: './templates/api/fetchByIdApi.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/{{layerName}}/{{sliceName}}/lib/query/useFetch{{sliceName}}ById.tsx',
+ templateFile: './templates/query/fetchByIdQuery.hbs',
+ },
+ ];
+ break;
+ case 'create':
+ actionList = [
+ {
+ type: 'add',
+ path: '../src/{{layerName}}/{{sliceName}}/api/create{{sliceName}}.ts',
+ templateFile: './templates/api/createApi.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/{{layerName}}/{{sliceName}}/lib/query/useCreate{{sliceName}}.tsx',
+ templateFile: './templates/query/createQuery.hbs',
+ },
+ ];
+ break;
+ case 'update':
+ actionList = [
+ {
+ type: 'add',
+ path: '../src/{{layerName}}/{{sliceName}}/api/update{{sliceName}}.ts',
+ templateFile: './templates/api/updateApi.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/{{layerName}}/{{sliceName}}/lib/query/useUpdate{{sliceName}}.tsx',
+ templateFile: './templates/query/updateQuery.hbs',
+ },
+ ];
+ break;
+ case 'delete':
+ actionList = [
+ {
+ type: 'add',
+ path: '../src/{{layerName}}/{{sliceName}}/api/delete{{sliceName}}.ts',
+ templateFile: './templates/api/deleteApi.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/{{layerName}}/{{sliceName}}/lib/query/useDelete{{sliceName}}.tsx',
+ templateFile: './templates/query/deleteQuery.hbs',
+ },
+ ];
+ break;
+ default:
+ console.log('Нет такого типа');
+ break;
+ }
+
+ return actionList;
+ },
+ });
+};
+
+module.exports = api;
diff --git a/plop/generation/common/component.js b/plop/generation/common/component.js
new file mode 100644
index 0000000000000000000000000000000000000000..f7e59e6669a2bd8f9ad55d51e7c7ff84ec163568
--- /dev/null
+++ b/plop/generation/common/component.js
@@ -0,0 +1,55 @@
+const component = (plop) => {
+ plop.setGenerator('component', {
+ description: 'Создает стандартный компонент',
+ prompts: [
+ {
+ type: 'list',
+ name: 'layerName',
+ message: 'В какой слой положить?',
+ choices: [
+ {
+ name: 'entities',
+ value: 'entities',
+ },
+ {
+ name: 'features',
+ value: 'features',
+ },
+ {
+ name: 'widgets',
+ value: 'widgets',
+ },
+ ],
+ },
+ {
+ type: 'input',
+ name: 'sliceName',
+ message: 'В какой слайс?',
+ },
+ {
+ type: 'input',
+ name: 'name',
+ message: 'Название компонента?',
+ },
+ ],
+ actions: [
+ {
+ type: 'add',
+ path: '../src/{{layerName}}/{{sliceName}}/ui/{{name}}/{{name}}.tsx',
+ templateFile: './templates/component/component.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/{{layerName}}/{{sliceName}}/ui/{{name}}/{{name}}.module.scss',
+ templateFile: './templates/component/component.style.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/{{layerName}}/{{sliceName}}/ui/{{name}}/{{name}}.stories.tsx',
+ templateFile: './templates/component/component.stories.hbs',
+ },
+ ],
+ });
+};
+
+module.exports = component;
diff --git a/plop/generation/common/form.js b/plop/generation/common/form.js
new file mode 100644
index 0000000000000000000000000000000000000000..710749da8f8c6e70ea2b4d598b830e0272a37c9e
--- /dev/null
+++ b/plop/generation/common/form.js
@@ -0,0 +1,55 @@
+const form = (plop) => {
+ plop.setGenerator('form', {
+ description: 'Создает стандартную форму',
+ prompts: [
+ {
+ type: 'list',
+ name: 'layerName',
+ message: 'В какой слой положить?',
+ choices: [
+ {
+ name: 'entities',
+ value: 'entities',
+ },
+ {
+ name: 'features',
+ value: 'features',
+ },
+ {
+ name: 'widgets',
+ value: 'widgets',
+ },
+ ],
+ },
+ {
+ type: 'input',
+ name: 'sliceName',
+ message: 'В какой слайс?',
+ },
+ {
+ type: 'input',
+ name: 'name',
+ message: 'Название компонента?',
+ },
+ ],
+ actions: [
+ {
+ type: 'add',
+ path: '../src/{{layerName}}/{{sliceName}}/ui/{{name}}/{{name}}.tsx',
+ templateFile: './templates/form/form.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/{{layerName}}/{{sliceName}}/ui/{{name}}/{{name}}.module.scss',
+ templateFile: './templates/form/form.style.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/{{layerName}}/{{sliceName}}/ui/{{name}}/{{name}}.stories.tsx',
+ templateFile: './templates/form/form.stories.hbs',
+ },
+ ],
+ });
+};
+
+module.exports = form;
diff --git a/plop/generation/common/slice.js b/plop/generation/common/slice.js
new file mode 100644
index 0000000000000000000000000000000000000000..7ee4324e9027be9598e97f5072086cc12c34f4ba
--- /dev/null
+++ b/plop/generation/common/slice.js
@@ -0,0 +1,55 @@
+const slice = (plop) => {
+ plop.setGenerator('slice', {
+ description: 'Создает слайс',
+ prompts: [
+ {
+ type: 'list',
+ name: 'layerName',
+ message: 'В какой слой положить?',
+ choices: [
+ {
+ name: 'entities',
+ value: 'entities',
+ },
+ {
+ name: 'features',
+ value: 'features',
+ },
+ {
+ name: 'widgets',
+ value: 'widgets',
+ },
+ ],
+ },
+ {
+ type: 'input',
+ name: 'name',
+ message: 'Название слайса?',
+ },
+ ],
+ actions: [
+ {
+ type: 'add',
+ path: '../src/{{layerName}}/{{name}}/ui/{{name}}.tsx',
+ templateFile: './templates/component/component.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/{{layerName}}/{{name}}/ui/{{name}}.module.scss',
+ templateFile: './templates/component/component.style.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/{{layerName}}/{{name}}/ui/{{name}}.stories.tsx',
+ templateFile: './templates/component/component.stories.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/{{layerName}}/{{name}}/index.ts',
+ templateFile: './templates/layers/rootIndex/rootIndex.hbs',
+ },
+ ],
+ });
+};
+
+module.exports = slice;
diff --git a/plop/generation/common/store.js b/plop/generation/common/store.js
new file mode 100644
index 0000000000000000000000000000000000000000..6bb9b5ed73c7dfd84ad73331720ad8f96285e43c
--- /dev/null
+++ b/plop/generation/common/store.js
@@ -0,0 +1,45 @@
+const store = (plop) => {
+ plop.setGenerator('store', {
+ description: 'Создает стор',
+ prompts: [
+ {
+ type: 'list',
+ name: 'layerName',
+ message: 'В какой слой положить?',
+ choices: [
+ {
+ name: 'entities',
+ value: 'entities',
+ },
+ {
+ name: 'features',
+ value: 'features',
+ },
+ {
+ name: 'widgets',
+ value: 'widgets',
+ },
+ ],
+ },
+ {
+ type: 'input',
+ name: 'sliceName',
+ message: 'В какой слайс?',
+ },
+ ],
+ actions: [
+ {
+ type: 'add',
+ path: '../src/{{layerName}}/{{sliceName}}/model/store/use{{sliceName}}Store.ts',
+ templateFile: './templates/store/store.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/{{layerName}}/{{sliceName}}/model/types/{{lowerCase sliceName}}Schema.ts',
+ templateFile: './templates/store/storeSchema.hbs',
+ },
+ ],
+ });
+};
+
+module.exports = store;
diff --git a/plop/generation/entities/entitiesComponent.js b/plop/generation/entities/entitiesComponent.js
new file mode 100644
index 0000000000000000000000000000000000000000..8f80c4f5c2af0120a1c07105bd11b158be247236
--- /dev/null
+++ b/plop/generation/entities/entitiesComponent.js
@@ -0,0 +1,40 @@
+const entitiesComponent = (plop) => {
+ plop.setGenerator('e-component', {
+ description: 'Создает стандартный компонент в сущности',
+ prompts: [
+ {
+ type: 'input',
+ name: 'name',
+ message: 'Название компонента?',
+ },
+ {
+ type: 'input',
+ name: 'sliceName',
+ message: 'Название слайса?',
+ },
+ ],
+ actions: (data) => {
+ data.layerName = 'entity';
+
+ return [
+ {
+ type: 'add',
+ path: '../src/entities/{{sliceName}}/ui/{{name}}/{{name}}.tsx',
+ templateFile: './templates/component/component.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/entities/{{sliceName}}/ui/{{name}}/{{name}}.module.scss',
+ templateFile: './templates/component/component.style.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/entities/{{sliceName}}/ui/{{name}}/{{name}}.stories.tsx',
+ templateFile: './templates/component/component.stories.hbs',
+ },
+ ];
+ },
+ });
+};
+
+module.exports = entitiesComponent;
diff --git a/plop/generation/entities/entity.js b/plop/generation/entities/entity.js
new file mode 100644
index 0000000000000000000000000000000000000000..0cf3449db881b0167354b94ed0dc1602769caab6
--- /dev/null
+++ b/plop/generation/entities/entity.js
@@ -0,0 +1,75 @@
+const entity = (plop) => {
+ plop.setGenerator('entity', {
+ description: 'Создает слайс в сущности',
+ prompts: [
+ {
+ type: 'input',
+ name: 'sliceName',
+ message: 'Название слайса?',
+ },
+ ],
+ actions: (data) => {
+ data.layerName = 'entity';
+
+ return [
+ {
+ type: 'add',
+ path: '../src/entities/{{sliceName}}/ui/{{sliceName}}Card/{{sliceName}}Card.tsx',
+ templateFile: './templates/layers/entities/cardUi/cardUi.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/entities/{{sliceName}}/ui/{{sliceName}}Card/{{sliceName}}Card.module.scss',
+ templateFile: './templates/layers/entities/cardUi/cardUi.style.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/entities/{{sliceName}}/ui/{{sliceName}}Card/{{sliceName}}Card.stories.tsx',
+ templateFile: './templates/layers/entities/cardUi/cardUi.stories.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/entities/{{sliceName}}/model/store/use{{sliceName}}Store.ts',
+ templateFile: './templates/layers/entities/store/store.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/entities/{{sliceName}}/model/types/{{lowerCase sliceName}}Schema.ts',
+ templateFile: './templates/layers/entities/types/sliceSchema.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/entities/{{sliceName}}/model/types/{{lowerCase sliceName}}.ts',
+ templateFile: './templates/layers/entities/types/slice.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/entities/{{sliceName}}/api/fetch{{sliceName}}s.ts',
+ templateFile: './templates/api/fetchApi.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/entities/{{sliceName}}/lib/query/useFetch{{sliceName}}s.tsx',
+ templateFile: './templates/query/fetchQuery.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/entities/{{sliceName}}/api/fetch{{sliceName}}ById.ts',
+ templateFile: './templates/api/fetchByIdApi.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/entities/{{sliceName}}/lib/query/useFetch{{sliceName}}ById.tsx',
+ templateFile: './templates/query/fetchByIdQuery.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/entities/{{sliceName}}/index.ts',
+ templateFile: './templates/layers/entities/rootIndex.hbs',
+ },
+ ];
+ },
+ });
+};
+
+module.exports = entity;
diff --git a/plop/generation/features/feature.js b/plop/generation/features/feature.js
new file mode 100644
index 0000000000000000000000000000000000000000..63071ee86f2fa226dc75aa68faab48fc264ae2ec
--- /dev/null
+++ b/plop/generation/features/feature.js
@@ -0,0 +1,95 @@
+const feature = (plop) => {
+ plop.setGenerator('feature', {
+ description: 'Создает слайс в фиче',
+ prompts: [
+ {
+ type: 'input',
+ name: 'sliceName',
+ message: 'Название слайса?',
+ },
+ ],
+ actions: (data) => {
+ data.layerName = 'feature';
+
+ return [
+ {
+ type: 'add',
+ path: '../src/features/{{sliceName}}/ui/Create{{sliceName}}/Create{{sliceName}}.tsx',
+ templateFile: './templates/layers/features/createUi/createUi.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/features/{{sliceName}}/ui/Create{{sliceName}}/Create{{sliceName}}.module.scss',
+ templateFile: './templates/layers/features/createUi/createUi.style.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/features/{{sliceName}}/ui/Edit{{sliceName}}/Edit{{sliceName}}.tsx',
+ templateFile: './templates/layers/features/editUi/editUi.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/features/{{sliceName}}/ui/Edit{{sliceName}}/Edit{{sliceName}}.module.scss',
+ templateFile: './templates/layers/features/editUi/editUi.style.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/features/{{sliceName}}/ui/Delete{{sliceName}}/Delete{{sliceName}}.tsx',
+ templateFile: './templates/layers/features/deleteUi/deleteUi.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/features/{{sliceName}}/ui/Delete{{sliceName}}/Delete{{sliceName}}.module.scss',
+ templateFile: './templates/layers/features/deleteUi/deleteUi.style.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/features/{{sliceName}}/model/store/use{{sliceName}}Store.ts',
+ templateFile: './templates/layers/features/store/store.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/features/{{sliceName}}/model/types/{{lowerCase sliceName}}Schema.ts',
+ templateFile: './templates/layers/features/types/sliceSchema.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/features/{{sliceName}}/api/create{{sliceName}}.ts',
+ templateFile: './templates/api/createApi.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/features/{{sliceName}}/lib/query/useCreate{{sliceName}}.tsx',
+ templateFile: './templates/query/createQuery.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/features/{{sliceName}}/api/update{{sliceName}}.ts',
+ templateFile: './templates/api/updateApi.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/features/{{sliceName}}/lib/query/useUpdate{{sliceName}}.tsx',
+ templateFile: './templates/query/updateQuery.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/features/{{sliceName}}/api/delete{{sliceName}}.ts',
+ templateFile: './templates/api/deleteApi.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/features/{{sliceName}}/lib/query/useDelete{{sliceName}}.tsx',
+ templateFile: './templates/query/deleteQuery.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/features/{{sliceName}}/index.ts',
+ templateFile: './templates/layers/features/rootIndex.hbs',
+ },
+ ];
+ },
+ });
+};
+
+module.exports = feature;
diff --git a/plop/generation/features/featuresComponent.js b/plop/generation/features/featuresComponent.js
new file mode 100644
index 0000000000000000000000000000000000000000..8945b1dc7e49e32337f70399feea669caee558a9
--- /dev/null
+++ b/plop/generation/features/featuresComponent.js
@@ -0,0 +1,40 @@
+const featuresComponent = (plop) => {
+ plop.setGenerator('f-component', {
+ description: 'Создает стандартный компонент в фиче',
+ prompts: [
+ {
+ type: 'input',
+ name: 'name',
+ message: 'Название компонента?',
+ },
+ {
+ type: 'input',
+ name: 'sliceName',
+ message: 'Название слайса?',
+ },
+ ],
+ actions: (data) => {
+ data.layerName = 'feature';
+
+ return [
+ {
+ type: 'add',
+ path: '../src/features/{{sliceName}}/ui/{{name}}/{{name}}.tsx',
+ templateFile: './templates/component/component.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/features/{{sliceName}}/ui/{{name}}/{{name}}.module.scss',
+ templateFile: './templates/component/component.style.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/features/{{sliceName}}/ui/{{name}}/{{name}}.stories.tsx',
+ templateFile: './templates/component/component.stories.hbs',
+ },
+ ];
+ },
+ });
+};
+
+module.exports = featuresComponent;
diff --git a/plop/generation/pages/page.js b/plop/generation/pages/page.js
new file mode 100644
index 0000000000000000000000000000000000000000..a11527573548aa2a37cc18e8167306dd41b1598c
--- /dev/null
+++ b/plop/generation/pages/page.js
@@ -0,0 +1,36 @@
+const page = (plop) => {
+ plop.setGenerator('page', {
+ description: 'Создает страницу',
+ prompts: [
+ {
+ type: 'input',
+ name: 'name',
+ message: 'Название страницы?',
+ },
+ ],
+ actions: [
+ {
+ type: 'add',
+ path: '../src/pages/{{name}}/ui/{{name}}.tsx',
+ templateFile: './templates/page/page.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/pages/{{name}}/ui/{{name}}.async.tsx',
+ templateFile: './templates/page/page.async.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/pages/{{name}}/ui/{{name}}.module.scss',
+ templateFile: './templates/page/page.style.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/pages/{{name}}/index.ts',
+ templateFile: './templates/page/index.hbs',
+ },
+ ],
+ });
+};
+
+module.exports = page;
diff --git a/plop/generation/shared/shared.js b/plop/generation/shared/shared.js
new file mode 100644
index 0000000000000000000000000000000000000000..775b886b0d113b0c141fcc3aec53cc5bd021f6ce
--- /dev/null
+++ b/plop/generation/shared/shared.js
@@ -0,0 +1,40 @@
+const shared = (plop) => {
+ plop.setGenerator('shared', {
+ description: 'Создает ui компонент в shared слое',
+ prompts: [
+ {
+ type: 'input',
+ name: 'name',
+ message: 'Название компонента?',
+ },
+ ],
+ actions: (data) => {
+ data.layerName = 'shared';
+
+ return [
+ {
+ type: 'add',
+ path: '../src/shared/ui/{{name}}/{{name}}.tsx',
+ templateFile: './templates/component/component.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/shared/ui/{{name}}/{{name}}.module.scss',
+ templateFile: './templates/component/component.style.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/shared/ui/{{name}}/{{name}}.stories.tsx',
+ templateFile: './templates/component/component.stories.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/shared/ui/{{name}}/index.ts',
+ templateFile: './templates/component/index.hbs',
+ },
+ ];
+ },
+ });
+};
+
+module.exports = shared;
diff --git a/plop/generation/widgets/widget.js b/plop/generation/widgets/widget.js
new file mode 100644
index 0000000000000000000000000000000000000000..1391089c1fbe1382375fff38645df19f1a88541a
--- /dev/null
+++ b/plop/generation/widgets/widget.js
@@ -0,0 +1,40 @@
+const widget = (plop) => {
+ plop.setGenerator('widget', {
+ description: 'Создает слайс в виджете',
+ prompts: [
+ {
+ type: 'input',
+ name: 'name',
+ message: 'Название слайса?',
+ },
+ ],
+ actions: (data) => {
+ data.layerName = 'widgets';
+
+ return [
+ {
+ type: 'add',
+ path: '../src/widgets/{{name}}/ui/{{name}}.tsx',
+ templateFile: './templates/component/component.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/widgets/{{name}}/ui/{{name}}.module.scss',
+ templateFile: './templates/component/component.style.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/widgets/{{name}}/ui/{{name}}.stories.tsx',
+ templateFile: './templates/component/component.stories.hbs',
+ },
+ {
+ type: 'add',
+ path: '../src/widgets/{{name}}/index.ts',
+ templateFile: './templates/layers/rootIndex/rootIndex.hbs',
+ },
+ ];
+ },
+ });
+};
+
+module.exports = widget;
diff --git a/plop/plopfile.js b/plop/plopfile.js
new file mode 100644
index 0000000000000000000000000000000000000000..c46e0f3f71f00217c02a80d22f81bce577afaa83
--- /dev/null
+++ b/plop/plopfile.js
@@ -0,0 +1,51 @@
+const api = require('./generation/common/api');
+const page = require('./generation/pages/page');
+const form = require('./generation/common/form');
+const store = require('./generation/common/store');
+const slice = require('./generation/common/slice');
+const shared = require('./generation/shared/shared');
+const widget = require('./generation/widgets/widget');
+const entity = require('./generation/entities/entity');
+const feature = require('./generation/features/feature');
+const component = require('./generation/common/component');
+const entitiesComponent = require('./generation/entities/entitiesComponent');
+const featuresComponent = require('./generation/features/featuresComponent');
+
+const config = (plop) => {
+ // Common
+ api(plop);
+ form(plop);
+ store(plop);
+ slice(plop);
+ component(plop);
+
+ // Entities
+ entity(plop);
+ entitiesComponent(plop);
+
+ // Features
+ feature(plop);
+ featuresComponent(plop);
+
+ // Pages
+ page(plop);
+
+ // Shared
+ shared(plop);
+
+ // Widget
+ widget(plop);
+
+ // Helpers
+ // Первая большая буква
+ plop.setHelper('capitalize', (text) => {
+ return text.charAt(0).toUpperCase() + text.slice(1);
+ });
+
+ // Все с маленькой
+ plop.setHelper('lowerCase', (text) => {
+ return text.toLowerCase();
+ });
+};
+
+module.exports = config;
diff --git a/plop/templates/api/createApi.hbs b/plop/templates/api/createApi.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..951f9c7f6ac42cf4151c5c4f6c7eb87ec592ae7d
--- /dev/null
+++ b/plop/templates/api/createApi.hbs
@@ -0,0 +1,18 @@
+import { $api } from '@/shared/api/axiosInstance';
+import { {{sliceName}}Type } from '@/entities/{{sliceName}}';
+
+type Create{{sliceName}}Props = {
+ user_id: number;
+};
+
+type Create{{sliceName}}Response = {
+ status: number;
+ message: string;
+ {{lowerCase sliceName}}: {{sliceName}}Type;
+};
+
+export const create{{sliceName}} = async (props: Create{{sliceName}}Props) => {
+ const { data } = await $api.post(`/{{lowerCase sliceName}}s/create`, props);
+
+ return data;
+};
diff --git a/plop/templates/api/deleteApi.hbs b/plop/templates/api/deleteApi.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..ff4e9f4e7b63402e71d1304aa3323ecda78a533b
--- /dev/null
+++ b/plop/templates/api/deleteApi.hbs
@@ -0,0 +1,15 @@
+import { $api } from '@/shared/api/axiosInstance';
+
+type Delete{{sliceName}}Props = {
+ {{lowerCase sliceName}}_id: number;
+};
+type Delete{{sliceName}}Response = {
+ status: number;
+ message: string;
+};
+
+export const delete{{sliceName}} = async (props: Delete{{sliceName}}Props) => {
+ const { data } = await $api.post(`/{{lowerCase sliceName}}s/delete`, props);
+
+ return data;
+};
diff --git a/plop/templates/api/fetchApi.hbs b/plop/templates/api/fetchApi.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..e82b12c3d95a4a3b81922a42788659696da7d58b
--- /dev/null
+++ b/plop/templates/api/fetchApi.hbs
@@ -0,0 +1,10 @@
+import { {{sliceName}}Type } from '../model/types/{{lowerCase sliceName}}';
+import { $api } from '@/shared/api/axiosInstance';
+
+type Fetch{{sliceName}}sResponse = {{sliceName}}Type[];
+
+export const fetch{{sliceName}}s = async () => {
+ const { data } = await $api.get(`/{{lowerCase sliceName}}s`);
+
+ return data;
+};
diff --git a/plop/templates/api/fetchByIdApi.hbs b/plop/templates/api/fetchByIdApi.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..8f5a7867d7e4011c7c49dff45671f56eed975039
--- /dev/null
+++ b/plop/templates/api/fetchByIdApi.hbs
@@ -0,0 +1,13 @@
+import { {{sliceName}}Type } from '../model/types/{{lowerCase sliceName}}';
+import { $api } from '@/shared/api/axiosInstance';
+
+type Fetch{{sliceName}}ByIdProps = {
+ {{lowerCase sliceName}}_id: number;
+};
+type Fetch{{sliceName}}ByIdResponse = {{sliceName}}Type;
+
+export const fetch{{sliceName}}ById = async (props: Fetch{{sliceName}}ByIdProps) => {
+ const { data } = await $api.post('/{{lowerCase sliceName}}-by-id', props);
+
+ return data;
+};
diff --git a/plop/templates/api/updateApi.hbs b/plop/templates/api/updateApi.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..13d850ac14ddda6fd250ec670c194430c19f82bc
--- /dev/null
+++ b/plop/templates/api/updateApi.hbs
@@ -0,0 +1,17 @@
+import { $api } from '@/shared/api/axiosInstance';
+
+type Update{{sliceName}}Props = {
+ {{lowerCase sliceName}}_id: number;
+ user_id: number;
+};
+
+type Update{{sliceName}}Response = {
+ status: number;
+ message: string;
+};
+
+export const update{{sliceName}} = async (props: Update{{sliceName}}Props) => {
+ const { data } = await $api.post(`/{{lowerCase sliceName}}s/update`, props);
+
+ return data;
+};
diff --git a/plop/templates/component/component.hbs b/plop/templates/component/component.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..8ea2cf14ef8b4bcc87984cb98a6c8de22ea8c40d
--- /dev/null
+++ b/plop/templates/component/component.hbs
@@ -0,0 +1,16 @@
+import { classNames } from '@/shared/lib/classNames/classNames';
+import cls from './{{name}}.module.scss';
+
+interface {{name}}Props {
+ className?: string;
+}
+
+export const {{name}} = (props: {{name}}Props) => {
+ const { className } = props;
+
+ return (
+
+ );
+};
diff --git a/plop/templates/component/component.stories.hbs b/plop/templates/component/component.stories.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..cb74643b631c6ac19713c1e3d867fa29918a369e
--- /dev/null
+++ b/plop/templates/component/component.stories.hbs
@@ -0,0 +1,18 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { {{name}} } from './{{name}}';
+
+const meta = {
+ title: '{{capitalize layerName}}/{{name}}',
+ component: {{name}},
+ parameters: {
+ layout: 'centered',
+ },
+ tags: ['autodocs'],
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Primary: Story = {
+ args: {},
+};
diff --git a/plop/templates/component/component.style.hbs b/plop/templates/component/component.style.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..7d03cfbc8fa2a3da36776b74c44a2ba5602915bb
--- /dev/null
+++ b/plop/templates/component/component.style.hbs
@@ -0,0 +1,5 @@
+@import '@/app/globalStyles/config/mixin.scss';
+
+.{{name}} {
+ display: block;
+}
diff --git a/plop/templates/component/index.hbs b/plop/templates/component/index.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..cbe8e73052d11a6039f8d0f57e05d00ffda05006
--- /dev/null
+++ b/plop/templates/component/index.hbs
@@ -0,0 +1 @@
+export { {{name}} } from './{{name}}';
diff --git a/plop/templates/form/form.hbs b/plop/templates/form/form.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..63abd4d7cedbd94bab5bcde1582a205a970c8910
--- /dev/null
+++ b/plop/templates/form/form.hbs
@@ -0,0 +1,72 @@
+import { z } from 'zod';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { FormProvider, useForm } from 'react-hook-form';
+import { HInput } from '@/shared/ui/FormComponents';
+import { classNames } from '@/shared/lib/classNames/classNames';
+import { Button, ButtonSize, ButtonTheme } from '@/shared/ui/Button';
+import cls from './{{name}}.module.scss';
+
+const {{name}}Schema = z.object({
+ title: z //
+ .string()
+ .min(1, { message: 'Заполните поле' })
+ .max(255),
+ body: z //
+ .string()
+ .min(1, { message: 'Заполните поле' })
+ .max(255),
+});
+
+type {{name}}Type = z.infer;
+
+interface {{name}}Props {
+ className?: string
+}
+
+const defaultValues = {
+ first: '',
+ second: '',
+};
+
+export const {{name}} = (props: {{name}}Props) => {
+ const { className } = props;
+
+ const methods = useForm<{{name}}Type>({
+ defaultValues,
+ resolver: zodResolver({{name}}Schema),
+ });
+
+ const { handleSubmit } = methods;
+
+ const submitHandler = async (data: {{name}}Type) => {
+ console.log('data: ', data);
+ };
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/plop/templates/form/form.stories.hbs b/plop/templates/form/form.stories.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..cb74643b631c6ac19713c1e3d867fa29918a369e
--- /dev/null
+++ b/plop/templates/form/form.stories.hbs
@@ -0,0 +1,18 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { {{name}} } from './{{name}}';
+
+const meta = {
+ title: '{{capitalize layerName}}/{{name}}',
+ component: {{name}},
+ parameters: {
+ layout: 'centered',
+ },
+ tags: ['autodocs'],
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Primary: Story = {
+ args: {},
+};
diff --git a/plop/templates/form/form.style.hbs b/plop/templates/form/form.style.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..d17c9040281bd5f6d272e53f9177638a4b389c00
--- /dev/null
+++ b/plop/templates/form/form.style.hbs
@@ -0,0 +1,9 @@
+@import '@/app/globalStyles/config/mixin.scss';
+
+.{{name}} {
+ display: block;
+}
+
+.field {
+ margin-bottom: 12px;
+}
diff --git a/plop/templates/layers/entities/cardUi/cardUi.hbs b/plop/templates/layers/entities/cardUi/cardUi.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..9a3566049ee5cb9cca8326291a394bd2e3b1ee4f
--- /dev/null
+++ b/plop/templates/layers/entities/cardUi/cardUi.hbs
@@ -0,0 +1,31 @@
+import { ReactNode } from 'react';
+import { classNames } from '@/shared/lib/classNames/classNames';
+import { {{sliceName}}Type } from '../../model/types/{{lowerCase sliceName}}';
+import cls from './{{sliceName}}Card.module.scss';
+
+interface {{sliceName}}CardProps {
+ className?: string;
+ {{lowerCase sliceName}}: {{sliceName}}Type;
+ editButton?: ReactNode;
+ deleteButton?: ReactNode;
+}
+
+export const {{sliceName}}Card = (props: {{sliceName}}CardProps) => {
+ const { className, {{lowerCase sliceName}}, editButton, deleteButton } = props;
+
+ return (
+
+
+
+ {
+ {{lowerCase sliceName}}.title
+ }
+
+
+ {editButton}
+ {deleteButton}
+
+
+
+ );
+};
diff --git a/plop/templates/layers/entities/cardUi/cardUi.stories.hbs b/plop/templates/layers/entities/cardUi/cardUi.stories.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..665046161c0652294a489e8e95244fa019bca310
--- /dev/null
+++ b/plop/templates/layers/entities/cardUi/cardUi.stories.hbs
@@ -0,0 +1,20 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { {{sliceName}}Card } from './{{sliceName}}Card';
+
+const meta = {
+ title: '{{capitalize layerName}}/{{sliceName}}Card',
+ component: {{sliceName}}Card,
+ parameters: {
+ layout: 'centered',
+ },
+ tags: ['autodocs'],
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Primary: Story = {
+ args: {
+ {{lowerCase sliceName}}: { id: 1, title: 'CardName' },
+ },
+};
diff --git a/plop/templates/layers/entities/cardUi/cardUi.style.hbs b/plop/templates/layers/entities/cardUi/cardUi.style.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..6d5783a0b77e3d2f2f9f9375e4b370d6d5db1d37
--- /dev/null
+++ b/plop/templates/layers/entities/cardUi/cardUi.style.hbs
@@ -0,0 +1,20 @@
+@import '@/app/globalStyles/config/mixin.scss';
+
+.{{sliceName}}Card {
+ display: block;
+}
+
+.content {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px;
+ border-radius: 8px;
+ border: 1px solid var(--s-accent-c);
+}
+
+.buttons {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
diff --git a/plop/templates/layers/entities/rootIndex.hbs b/plop/templates/layers/entities/rootIndex.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..538641563b77101a72a07e70a7af4910ba807908
--- /dev/null
+++ b/plop/templates/layers/entities/rootIndex.hbs
@@ -0,0 +1,4 @@
+export { {{sliceName}}Card } from './ui/{{sliceName}}Card/{{sliceName}}Card';
+export type { {{sliceName}}Type } from './model/types/post';
+export { useFetch{{sliceName}}s } from './lib/query/useFetch{{sliceName}}s';
+export { useFetch{{sliceName}}ById } from './lib/query/useFetch{{sliceName}}ById';
diff --git a/plop/templates/layers/entities/store/store.hbs b/plop/templates/layers/entities/store/store.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..7dda49d49c7ac294cc4971324b9ec1252dbc9505
--- /dev/null
+++ b/plop/templates/layers/entities/store/store.hbs
@@ -0,0 +1,10 @@
+import { create } from 'zustand';
+import { devtools } from 'zustand/middleware';
+import { {{sliceName}}Schema } from '../types/{{lowerCase sliceName}}Schema';
+
+export const use{{sliceName}}Store = create<{{sliceName}}Schema>()(
+ devtools((set, get) => ({
+ isModalActive: false,
+ toggleModal: () => set({ isModalActive: !get().isModalActive }),
+ }))
+);
diff --git a/plop/templates/layers/entities/types/slice.hbs b/plop/templates/layers/entities/types/slice.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..1c52682f14d07169ef0549b5d6e36b17e0228b2a
--- /dev/null
+++ b/plop/templates/layers/entities/types/slice.hbs
@@ -0,0 +1,4 @@
+export interface {{sliceName}}Type {
+ id: number;
+ title: string;
+}
diff --git a/plop/templates/layers/entities/types/sliceSchema.hbs b/plop/templates/layers/entities/types/sliceSchema.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..c3af01aa7e3202d4bb8943ce407f33221e6be82b
--- /dev/null
+++ b/plop/templates/layers/entities/types/sliceSchema.hbs
@@ -0,0 +1,4 @@
+export interface {{sliceName}}Schema {
+ isModalActive: boolean;
+ toggleModal: () => void;
+}
diff --git a/plop/templates/layers/features/createUi/createUi.hbs b/plop/templates/layers/features/createUi/createUi.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..9a15ed99dc6bd4ff6dd00f8e1e953c2c1fe1ce7a
--- /dev/null
+++ b/plop/templates/layers/features/createUi/createUi.hbs
@@ -0,0 +1,30 @@
+import { classNames } from '@/shared/lib/classNames/classNames';
+import { Button, ButtonSize, ButtonTheme } from '@/shared/ui/Button';
+import { use{{sliceName}}Store } from '../../model/store/use{{sliceName}}Store';
+import cls from './Create{{sliceName}}.module.scss';
+
+interface Create{{sliceName}}Props {
+ className?: string;
+}
+
+export const Create{{sliceName}} = (props: Create{{sliceName}}Props) => {
+ const { className } = props;
+ const toggleModal = use{{sliceName}}Store((state) => state.toggleModal);
+ const changingEditable{{sliceName}} = use{{sliceName}}Store((state) => state.changingEditable{{sliceName}});
+
+ const openEdit{{sliceName}}Form = () => {
+ changingEditable{{sliceName}}(undefined);
+ toggleModal();
+ };
+
+ return (
+
+ );
+};
diff --git a/plop/templates/layers/features/createUi/createUi.style.hbs b/plop/templates/layers/features/createUi/createUi.style.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..0879c08fe753c0beb3ea252e0d85ce92cc8d6e7e
--- /dev/null
+++ b/plop/templates/layers/features/createUi/createUi.style.hbs
@@ -0,0 +1,5 @@
+@import '@/app/globalStyles/config/mixin.scss';
+
+.Create{{sliceName}} {
+ margin-bottom: 48px;
+}
diff --git a/plop/templates/layers/features/deleteUi/deleteUi.hbs b/plop/templates/layers/features/deleteUi/deleteUi.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..6f6942425ddc0624c16739bd7f964703a2120839
--- /dev/null
+++ b/plop/templates/layers/features/deleteUi/deleteUi.hbs
@@ -0,0 +1,27 @@
+import { classNames } from '@/shared/lib/classNames/classNames';
+import { Button, ButtonSize, ButtonTheme } from '@/shared/ui/Button';
+import { useDelete{{sliceName}} } from '../../lib/query/useDelete{{sliceName}}';
+import cls from './Delete{{sliceName}}.module.scss';
+
+interface Delete{{sliceName}}Props {
+ className?: string;
+ {{lowerCase sliceName}}Id: number;
+}
+
+export const Delete{{sliceName}} = (props: Delete{{sliceName}}Props) => {
+ const { className, {{lowerCase sliceName}}Id } = props;
+ const { mutate: onDelete } = useDelete{{sliceName}}();
+
+ return (
+
+ );
+};
diff --git a/plop/templates/layers/features/deleteUi/deleteUi.style.hbs b/plop/templates/layers/features/deleteUi/deleteUi.style.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..a925e3fd59cac2a8b473c5fb7cf3bcba20f83ec3
--- /dev/null
+++ b/plop/templates/layers/features/deleteUi/deleteUi.style.hbs
@@ -0,0 +1,5 @@
+@import '@/app/globalStyles/config/mixin.scss';
+
+.Delete{{sliceName}} {
+ display: block;
+}
diff --git a/plop/templates/layers/features/editUi/editUi.hbs b/plop/templates/layers/features/editUi/editUi.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..29d846c7fa67fb20cb20578e01ace919b7d3efeb
--- /dev/null
+++ b/plop/templates/layers/features/editUi/editUi.hbs
@@ -0,0 +1,31 @@
+import { classNames } from '@/shared/lib/classNames/classNames';
+import { Button, ButtonSize, ButtonTheme } from '@/shared/ui/Button';
+import { use{{sliceName}}Store } from '../../model/store/use{{sliceName}}Store';
+import cls from './Edit{{sliceName}}.module.scss';
+
+interface Edit{{sliceName}}Props {
+ className?: string;
+ {{sliceName}}Id: number;
+}
+
+export const Edit{{sliceName}} = (props: Edit{{sliceName}}Props) => {
+ const { className, {{sliceName}}Id } = props;
+ const toggleModal = use{{sliceName}}Store((state) => state.toggleModal);
+ const changingEditable{{sliceName}} = use{{sliceName}}Store((state) => state.changingEditable{{sliceName}});
+
+ const openEdit{{sliceName}}Form = (id: number) => {
+ changingEditable{{sliceName}}(id);
+ toggleModal();
+ };
+
+ return (
+
+ );
+};
diff --git a/plop/templates/layers/features/editUi/editUi.style.hbs b/plop/templates/layers/features/editUi/editUi.style.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..03b746f3895c9a33c298ae3b1f0f8db3c881c478
--- /dev/null
+++ b/plop/templates/layers/features/editUi/editUi.style.hbs
@@ -0,0 +1,5 @@
+@import '@/app/globalStyles/config/mixin.scss';
+
+.Edit{{sliceName}} {
+ display: block;
+}
diff --git a/plop/templates/layers/features/rootIndex.hbs b/plop/templates/layers/features/rootIndex.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..a2831909c919d065236cfb8359012987c4dfcdcd
--- /dev/null
+++ b/plop/templates/layers/features/rootIndex.hbs
@@ -0,0 +1,4 @@
+export { Create{{sliceName}} } from './ui/Create{{sliceName}}/Create{{sliceName}}';
+export { Edit{{sliceName}} } from './ui/Edit{{sliceName}}/Edit{{sliceName}}';
+export { use{{sliceName}}Store } from './model/store/use{{sliceName}}Store';
+export { Delete{{sliceName}} } from './ui/Delete{{sliceName}}/Delete{{sliceName}}';
diff --git a/plop/templates/layers/features/store/store.hbs b/plop/templates/layers/features/store/store.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..57b74bbab1c92cc4dcbf4126eb8bcd333bbaea2b
--- /dev/null
+++ b/plop/templates/layers/features/store/store.hbs
@@ -0,0 +1,12 @@
+import { create } from 'zustand';
+import { devtools } from 'zustand/middleware';
+import { {{sliceName}}Schema } from '../types/{{lowerCase sliceName}}Schema';
+
+export const use{{sliceName}}Store = create<{{sliceName}}Schema>()(
+ devtools((set, get) => ({
+ isModalActive: false,
+ editable{{sliceName}}: undefined,
+ toggleModal: () => set({ isModalActive: !get().isModalActive }),
+ changingEditable{{sliceName}}: ({{lowerCase sliceName}}Id) => set({ editable{{sliceName}}Id: {{lowerCase sliceName}}Id }),
+ }))
+);
diff --git a/plop/templates/layers/features/types/sliceSchema.hbs b/plop/templates/layers/features/types/sliceSchema.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..7bea3e3353a68689dba9cdd48e140dc547ace856
--- /dev/null
+++ b/plop/templates/layers/features/types/sliceSchema.hbs
@@ -0,0 +1,6 @@
+export interface {{sliceName}}Schema {
+ isModalActive: boolean;
+ editable{{sliceName}}Id?: number;
+ toggleModal: () => void;
+ changingEditable{{sliceName}}: ({{lowerCase sliceName}}Id?: number) => void;
+}
diff --git a/plop/templates/layers/rootIndex/rootIndex.hbs b/plop/templates/layers/rootIndex/rootIndex.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..5694f503a6f8fbcf7b6d25ce0426ab9ddfc90765
--- /dev/null
+++ b/plop/templates/layers/rootIndex/rootIndex.hbs
@@ -0,0 +1 @@
+export { {{name}} } from './ui/{{name}}';
diff --git a/plop/templates/page/index.hbs b/plop/templates/page/index.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..596a8b0c56c195426a9442402bcd901bc7d418d1
--- /dev/null
+++ b/plop/templates/page/index.hbs
@@ -0,0 +1 @@
+export { {{name}}Async as {{name}} } from './ui/{{name}}.async';
diff --git a/plop/templates/page/page.async.hbs b/plop/templates/page/page.async.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..9800b25e57d8040c5b28d69cfdec8aabc9c23b8b
--- /dev/null
+++ b/plop/templates/page/page.async.hbs
@@ -0,0 +1,3 @@
+import { lazy } from 'react';
+
+export const {{name}}Async = lazy(() => import('./{{name}}'));
diff --git a/plop/templates/page/page.hbs b/plop/templates/page/page.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..73f1a3b5aa3bfb3d991da8dd85d67472e02a0b85
--- /dev/null
+++ b/plop/templates/page/page.hbs
@@ -0,0 +1,11 @@
+import { Wrapper } from '@/shared/ui/Wrapper';
+
+const {{name}} = () => {
+ return (
+
+ {{name}}
+
+ );
+};
+
+export default {{name}};
diff --git a/plop/templates/page/page.style.hbs b/plop/templates/page/page.style.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..7d03cfbc8fa2a3da36776b74c44a2ba5602915bb
--- /dev/null
+++ b/plop/templates/page/page.style.hbs
@@ -0,0 +1,5 @@
+@import '@/app/globalStyles/config/mixin.scss';
+
+.{{name}} {
+ display: block;
+}
diff --git a/plop/templates/query/createQuery.hbs b/plop/templates/query/createQuery.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..ed9a2ee94f6db580d683bf1a04f7521240a5e4eb
--- /dev/null
+++ b/plop/templates/query/createQuery.hbs
@@ -0,0 +1,13 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { create{{sliceName}} } from '../../api/create{{sliceName}}';
+
+export const useCreate{{sliceName}} = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: create{{sliceName}},
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['fetch{{sliceName}}s'] });
+ },
+ });
+};
diff --git a/plop/templates/query/deleteQuery.hbs b/plop/templates/query/deleteQuery.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..69d481bd2ead4e8752084f2546943856c1053f58
--- /dev/null
+++ b/plop/templates/query/deleteQuery.hbs
@@ -0,0 +1,13 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { delete{{sliceName}} } from '../../api/delete{{sliceName}}';
+
+export const useDelete{{sliceName}} = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: delete{{sliceName}},
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['fetch{{sliceName}}s'] });
+ },
+ });
+};
diff --git a/plop/templates/query/fetchByIdQuery.hbs b/plop/templates/query/fetchByIdQuery.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..2ddbb4fe388b1d84231bb877b0b456d5bef8344e
--- /dev/null
+++ b/plop/templates/query/fetchByIdQuery.hbs
@@ -0,0 +1,14 @@
+import { useQuery } from '@tanstack/react-query';
+import { fetch{{sliceName}}ById } from '../../api/fetch{{sliceName}}ById';
+
+export const useFetch{{sliceName}}ById = ({{lowerCase sliceName}}Id: number | undefined) => {
+
+ return useQuery({
+ queryKey: ['fetch{{sliceName}}ById', {{lowerCase sliceName}}Id],
+ queryFn: () =>
+ fetch{{sliceName}}ById({
+ {{lowerCase sliceName}}_id: {{lowerCase sliceName}}Id || 1
+ }),
+ enabled: !!{{lowerCase sliceName}}Id,
+ });
+};
diff --git a/plop/templates/query/fetchQuery.hbs b/plop/templates/query/fetchQuery.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..6a030f439704c5789e62777ea3e90a488d538872
--- /dev/null
+++ b/plop/templates/query/fetchQuery.hbs
@@ -0,0 +1,9 @@
+import { useQuery } from '@tanstack/react-query';
+import { fetch{{sliceName}}s } from '../../api/fetch{{sliceName}}s';
+
+export const useFetch{{sliceName}}s = () => {
+ return useQuery({
+ queryKey: ['fetch{{sliceName}}s'],
+ queryFn: () => fetch{{sliceName}}s(),
+ });
+};
diff --git a/plop/templates/query/updateQuery.hbs b/plop/templates/query/updateQuery.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..3e5f05db7ea5d2163a8ef3e2561bbb6dd6c6680c
--- /dev/null
+++ b/plop/templates/query/updateQuery.hbs
@@ -0,0 +1,13 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { update{{sliceName}} } from '../../api/update{{sliceName}}';
+
+export const useUpdate{{sliceName}} = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: update{{sliceName}},
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['fetch{{sliceName}}s'] });
+ },
+ });
+};
diff --git a/plop/templates/store/store.hbs b/plop/templates/store/store.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..7dda49d49c7ac294cc4971324b9ec1252dbc9505
--- /dev/null
+++ b/plop/templates/store/store.hbs
@@ -0,0 +1,10 @@
+import { create } from 'zustand';
+import { devtools } from 'zustand/middleware';
+import { {{sliceName}}Schema } from '../types/{{lowerCase sliceName}}Schema';
+
+export const use{{sliceName}}Store = create<{{sliceName}}Schema>()(
+ devtools((set, get) => ({
+ isModalActive: false,
+ toggleModal: () => set({ isModalActive: !get().isModalActive }),
+ }))
+);
diff --git a/plop/templates/store/storeSchema.hbs b/plop/templates/store/storeSchema.hbs
new file mode 100644
index 0000000000000000000000000000000000000000..c3af01aa7e3202d4bb8943ce407f33221e6be82b
--- /dev/null
+++ b/plop/templates/store/storeSchema.hbs
@@ -0,0 +1,4 @@
+export interface {{sliceName}}Schema {
+ isModalActive: boolean;
+ toggleModal: () => void;
+}
diff --git a/src/app/App.tsx b/src/app/App.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..c8d9c5b9b08255103525bb4119a1ad50395d1671
--- /dev/null
+++ b/src/app/App.tsx
@@ -0,0 +1,16 @@
+import { RouterProvider } from '@/app/providers/RouterProvider';
+import { QueryProvider } from '@/app/providers/QueryProvider';
+import { ThemeProvider } from '@/app/providers/ThemeProviders';
+import '@/app/globalStyles/styles.scss';
+
+function App() {
+ return (
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/src/app/globalStyles/config/mixin.scss b/src/app/globalStyles/config/mixin.scss
new file mode 100644
index 0000000000000000000000000000000000000000..5ae6a19aed5e2e66aa94a94fd7aa5e2a98e69935
--- /dev/null
+++ b/src/app/globalStyles/config/mixin.scss
@@ -0,0 +1,31 @@
+// Медиа запросы для разных ширин экрана
+// Пример использования: @include desktop-1200 {font-size: 20px;}
+@mixin before-1366 {
+ @media screen and (max-width: 1365px) {
+ @content;
+ }
+}
+
+@mixin before-1200 {
+ @media screen and (max-width: 1199px) {
+ @content;
+ }
+}
+
+@mixin before-1024 {
+ @media screen and (max-width: 1023px) {
+ @content;
+ }
+}
+
+@mixin before-768 {
+ @media screen and (max-width: 767px) {
+ @content;
+ }
+}
+
+@mixin before-480 {
+ @media screen and (max-width: 479px) {
+ @content;
+ }
+}
diff --git a/src/app/globalStyles/dark.scss b/src/app/globalStyles/dark.scss
new file mode 100644
index 0000000000000000000000000000000000000000..bb51a9f96864b3ad1a549c53c380265820db8a74
--- /dev/null
+++ b/src/app/globalStyles/dark.scss
@@ -0,0 +1,33 @@
+body.dark {
+ //Text colors
+ --t-primery-c: var(--color-white);
+ --t-accent-c: var(--color-green);
+ --t-accent-c-2: var(--color-green2);
+ --t-inverted-c: var(--color-white);
+ --t-descriptive-c-2: var(--color-gray2);
+ --t-descriptive-c-3: var(--color-gray4);
+ --t-descriptive-c-4: var(--color-blue);
+ --t-descriptive-c-5: var(--color-gray5);
+
+ //Surface colors
+ --s-primery-c: var(--color-green2);
+ --s-inverted-c: var(--color-white);
+ --s-accent-c: var(--color-green);
+ --s-secondary-c-1: var(--color-black);
+ --s-secondary-c-2: var(--color-gray4);
+ --s-secondary-c-3: var(--color-gray5);
+ --s-secondary-c-4: var(--color-rgba255);
+ --s-secondary-c-5: var(--color-gray2);
+ --s-secondary-c-6: var(--color-blue);
+ --s-secondary-c-7: var(--color-gray3);
+ --s-secondary-c-8: var(--color-gray7);
+ --s-secondary-c-9: var(--color-gray6);
+ --s-secondary-c-10: var(--color-gray8);
+ --s-gradient-c-1: var(--color-gradientGreen);
+ --s-gradient-c-2: var(--color-gradientGreen2);
+ --s-gradient-c-3: var(--color-gradientGreen3);
+ --error-primery-color: var(--color-red-1);
+
+ //Background colors
+ --bg-primery-c: var(--color-black);
+}
diff --git a/src/app/globalStyles/fonts.scss b/src/app/globalStyles/fonts.scss
new file mode 100644
index 0000000000000000000000000000000000000000..25d086ccf3782210ff0656e0d341e70b01e2e54e
--- /dev/null
+++ b/src/app/globalStyles/fonts.scss
@@ -0,0 +1,39 @@
+@font-face {
+ font-family: 'SBSansDisplay';
+ font-weight: 100;
+ font-style: normal;
+ src: url('../../shared/assets/fonts/SBSansDisplay-Thin.woff2') format('woff2');
+ font-display: swap;
+}
+
+@font-face {
+ font-family: 'SBSansDisplay';
+ font-weight: 300;
+ font-style: normal;
+ src: url('../../shared/assets/fonts/SBSansDisplay-Light.woff2') format('woff2');
+ font-display: swap;
+}
+
+@font-face {
+ font-family: 'SBSansDisplay';
+ font-weight: 400;
+ font-style: normal;
+ src: url('../../shared/assets/fonts/SBSansDisplay-Regular.woff2') format('woff2');
+ font-display: swap;
+}
+
+@font-face {
+ font-family: 'SBSansDisplay';
+ font-weight: 600;
+ font-style: normal;
+ src: url('../../shared/assets/fonts/SBSansDisplay-SemiBold.woff2') format('woff2');
+ font-display: swap;
+}
+
+@font-face {
+ font-family: 'SBSansDisplay';
+ font-weight: 700;
+ font-style: normal;
+ src: url('../../shared/assets/fonts/SBSansDisplay-Bold.woff2') format('woff2');
+ font-display: swap;
+}
diff --git a/src/app/globalStyles/light.scss b/src/app/globalStyles/light.scss
new file mode 100644
index 0000000000000000000000000000000000000000..1c7d8d86471a1e738080f22b34676b7c6c506832
--- /dev/null
+++ b/src/app/globalStyles/light.scss
@@ -0,0 +1,33 @@
+body.light {
+ //Text colors
+ --t-primery-c: var(--color-black);
+ --t-accent-c: var(--color-green);
+ --t-accent-c-2: var(--color-green2);
+ --t-inverted-c: var(--color-white);
+ --t-descriptive-c-2: var(--color-gray2);
+ --t-descriptive-c-3: var(--color-gray4);
+ --t-descriptive-c-4: var(--color-blue);
+ --t-descriptive-c-5: var(--color-gray5);
+
+ //Surface colors
+ --s-primery-c: var(--color-green2);
+ --s-inverted-c: var(--color-white);
+ --s-accent-c: var(--color-green);
+ --s-secondary-c-1: var(--color-black);
+ --s-secondary-c-2: var(--color-gray4);
+ --s-secondary-c-3: var(--color-gray5);
+ --s-secondary-c-4: var(--color-rgba255);
+ --s-secondary-c-5: var(--color-gray2);
+ --s-secondary-c-6: var(--color-blue);
+ --s-secondary-c-7: var(--color-gray3);
+ --s-secondary-c-8: var(--color-gray7);
+ --s-secondary-c-9: var(--color-gray6);
+ --s-secondary-c-10: var(--color-gray8);
+ --s-gradient-c-1: var(--color-gradientGreen);
+ --s-gradient-c-2: var(--color-gradientGreen2);
+ --s-gradient-c-3: var(--color-gradientGreen3);
+ --error-primery-color: var(--color-red-1);
+
+ //Background colors
+ --bg-primery-c: var(--color-gray6);
+}
diff --git a/src/app/globalStyles/palette.scss b/src/app/globalStyles/palette.scss
new file mode 100644
index 0000000000000000000000000000000000000000..fb61efb50edaf6452f3d87556b9344af13ebeacf
--- /dev/null
+++ b/src/app/globalStyles/palette.scss
@@ -0,0 +1,19 @@
+:root {
+ --font-primery: 'SBSansDisplay', 'MuseoSansCyrl', 'Arial', sans-serif;
+ --color-white: #ffffff;
+ --color-black: #30373c;
+ --color-gray2: #657179;
+ --color-gray4: #8a959d;
+ --color-green: #0c9c0c;
+ --color-green2: #15b015;
+ --color-gray3: #c0cbd3;
+ --color-gray5: #d5dfe6;
+ --color-gray6: #f2f5f8;
+ --color-gray7: #f2f5f8;
+ --color-gray8: #e8eef2;
+ --color-red-1: #e64646;
+ --color-rgba255: rgba(255, 255, 255, 0.3);
+ --color-gradientGreen: linear-gradient(104deg, #17d317 0%, #14cc98 100%);
+ --color-gradientGreen2: linear-gradient(162deg, #f9ee2f 0%, #f0e405 9.9%, #17d317 37.48%, #17d317 77.23%);
+ --color-gradientGreen3: linear-gradient(48deg, #3ce73c 21.66%, #4ad6fc 83.62%);
+}
diff --git a/src/app/globalStyles/reset.scss b/src/app/globalStyles/reset.scss
new file mode 100644
index 0000000000000000000000000000000000000000..a3f0a392fd98353b564f3408912fc04c8551781d
--- /dev/null
+++ b/src/app/globalStyles/reset.scss
@@ -0,0 +1,83 @@
+/**
+Andy Bell
+https://hankchizljaw.com/wrote/a-modern-css-reset/
+ */
+
+/* Box sizing rules */
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+/* Remove default padding */
+ul,
+ol {
+ padding: 0;
+}
+
+/* Remove default margin */
+body,
+h1,
+h2,
+h3,
+h4,
+p,
+ul,
+ol,
+li,
+figure,
+figcaption,
+blockquote,
+dl,
+dd {
+ margin: 0;
+}
+
+/* Remove list styles on ul, ol elements with a class attribute */
+/* stylelint-disable */
+ul,
+ol {
+ list-style: none;
+}
+/* stylelint-enable */
+
+/* A elements that don't have a class get default styles */
+a:not([class]) {
+ text-decoration-skip-ink: auto;
+}
+
+/* Make images easier to work with */
+img {
+ display: block;
+ max-width: 100%;
+}
+
+/* Inherit fonts for inputs and buttons */
+input,
+button,
+textarea,
+select {
+ font: inherit;
+}
+
+/* Remove all animations and transitions for people that prefer not to see them */
+@media (prefers-reduced-motion: reduce) {
+ * {
+ transition-duration: 0.01ms !important;
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ scroll-behavior: auto !important;
+ }
+}
+
+input::-webkit-outer-spin-button,
+input::-webkit-inner-spin-button {
+ /* display: none; <- Crashes Chrome on hover */
+ appearance: none;
+ margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
+}
+
+input[type='number'] {
+ appearance: textfield; /* Firefox */
+}
diff --git a/src/app/globalStyles/scaffolding.scss b/src/app/globalStyles/scaffolding.scss
new file mode 100644
index 0000000000000000000000000000000000000000..00dd90c7088d7aad90ed84f646e8de1c8182f23e
--- /dev/null
+++ b/src/app/globalStyles/scaffolding.scss
@@ -0,0 +1,37 @@
+html,
+body {
+ height: 100%;
+}
+
+body {
+ margin: 0;
+ font-weight: 400;
+ font-family: var(--font-primery);
+ font-size: 16px;
+ line-height: normal;
+ color: var(--t-primery-c);
+ background-color: var(--bg-primery-c);
+ scrollbar-gutter: stable;
+}
+
+a:hover {
+ text-decoration: none;
+}
+
+.visually-hidden:not(:focus):not(:active),
+input[type='checkbox'].visually-hidden,
+input[type='radio'].visually-hidden {
+ position: absolute;
+
+ width: 1px;
+ height: 1px;
+ margin: -1px;
+ border: 0;
+ padding: 0;
+
+ white-space: nowrap;
+
+ clip-path: inset(100%);
+ clip: rect(0 0 0 0);
+ overflow: hidden;
+}
diff --git a/src/app/globalStyles/styles.scss b/src/app/globalStyles/styles.scss
new file mode 100644
index 0000000000000000000000000000000000000000..18957299657541019049bc0d61d69588d8e11e37
--- /dev/null
+++ b/src/app/globalStyles/styles.scss
@@ -0,0 +1,6 @@
+@import './fonts';
+@import './reset.scss';
+@import './palette.scss';
+@import './light.scss';
+@import './dark.scss';
+@import './scaffolding.scss';
diff --git a/src/app/layouts/MainLayout.tsx b/src/app/layouts/MainLayout.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f3661d1272872798ad4625217f4914b59feaa8e0
--- /dev/null
+++ b/src/app/layouts/MainLayout.tsx
@@ -0,0 +1,14 @@
+import { Outlet } from 'react-router-dom';
+import { Header } from '@/widgets/Header';
+import { Layout } from '@/shared/ui/Layout';
+import { SideMenu } from '@/widgets/SideMenu';
+
+export const MainLayout = () => {
+ return (
+ }
+ pageSlot={}
+ sideMenuSlot={}
+ />
+ );
+};
diff --git a/src/app/providers/QueryProvider/index.ts b/src/app/providers/QueryProvider/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..74d11c7991fe945b8af3cbbe9869f14fc3c8ea1f
--- /dev/null
+++ b/src/app/providers/QueryProvider/index.ts
@@ -0,0 +1,3 @@
+import QueryProvider from './ui/QueryProvider';
+
+export { QueryProvider };
diff --git a/src/app/providers/QueryProvider/ui/QueryProvider.tsx b/src/app/providers/QueryProvider/ui/QueryProvider.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..753b0f71ec1fedf3b1de3dce908ed1318d6c034c
--- /dev/null
+++ b/src/app/providers/QueryProvider/ui/QueryProvider.tsx
@@ -0,0 +1,15 @@
+import { ReactNode } from 'react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+
+const queryClient = new QueryClient();
+
+interface IQueryProvider {
+ children: ReactNode;
+}
+
+const QueryProvider = (props: IQueryProvider) => {
+ const { children } = props;
+ return {children};
+};
+
+export default QueryProvider;
diff --git a/src/app/providers/RouterProvider/index.ts b/src/app/providers/RouterProvider/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..21918619c5f76c12cf5afff83f5e2a3c16b18a70
--- /dev/null
+++ b/src/app/providers/RouterProvider/index.ts
@@ -0,0 +1,3 @@
+import RouterProvider from './ui/RouterProvider';
+
+export { RouterProvider };
diff --git a/src/app/providers/RouterProvider/ui/ProtectedRoute.tsx b/src/app/providers/RouterProvider/ui/ProtectedRoute.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..577b24a0725a2a74f582db5a358dae60703fc627
--- /dev/null
+++ b/src/app/providers/RouterProvider/ui/ProtectedRoute.tsx
@@ -0,0 +1,20 @@
+import { Navigate, Outlet } from 'react-router-dom';
+import { useUserStore } from '@/entities/User/model/store/useUserStore';
+import { USER_AUTH_DATA } from '@/shared/const/localStorage';
+
+interface ProtectedRouteProps {
+ redirectPath?: string;
+}
+
+export const ProtectedRoute = (props: ProtectedRouteProps) => {
+ const { redirectPath } = props;
+ const setAuthUser = useUserStore((state) => state.setAuthUser);
+ const user = localStorage.getItem(USER_AUTH_DATA);
+
+ if (user) {
+ setAuthUser(JSON.parse(user));
+ return ;
+ }
+
+ return ;
+};
diff --git a/src/app/providers/RouterProvider/ui/RouterProvider.tsx b/src/app/providers/RouterProvider/ui/RouterProvider.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..5ed851c79f7a9c6141da6e943deed01c71450ecd
--- /dev/null
+++ b/src/app/providers/RouterProvider/ui/RouterProvider.tsx
@@ -0,0 +1,43 @@
+import { Suspense } from 'react';
+import { createBrowserRouter, RouterProvider as RouterProviderLib } from 'react-router-dom';
+import { MainPage } from '@/pages/MainPage';
+import { LoginPage } from '@/pages/LoginPage';
+import { ProtectedRoute } from './ProtectedRoute';
+import { ContactPage } from '@/pages/ContactPage';
+import { NotFoundPage } from '@/pages/NotFoundPage';
+import { SomeTestPage } from '@/pages/SomeTestPage';
+import { PageLoader } from '@/shared/ui/PageLoader';
+import { MainLayout } from '@/app/layouts/MainLayout';
+import { useTheme } from '@/app/providers/ThemeProviders';
+
+const router = createBrowserRouter([
+ { path: '/login', element: },
+ {
+ element: ,
+ children: [
+ {
+ path: '/',
+ element: ,
+ children: [
+ { index: true, element: },
+ { path: 'contact', element: },
+ { path: 'someTests', element: },
+ ],
+ },
+ ],
+ },
+ { path: '*', element: },
+]);
+
+const RouterProvider = () => {
+ const { theme } = useTheme();
+ document.body.className = theme;
+
+ return (
+ }>
+
+
+ );
+};
+
+export default RouterProvider;
diff --git a/src/app/providers/ThemeProviders/index.tsx b/src/app/providers/ThemeProviders/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..1de738bf32b8689f8c76add26eb79b00aa3a1714
--- /dev/null
+++ b/src/app/providers/ThemeProviders/index.tsx
@@ -0,0 +1,5 @@
+import ThemeProvider from './ui/ThemeProvider';
+import { useTheme } from './lib/useTheme';
+import { Theme } from './lib/ThemeContext';
+
+export { ThemeProvider, useTheme, Theme };
diff --git a/src/app/providers/ThemeProviders/lib/ThemeContext.ts b/src/app/providers/ThemeProviders/lib/ThemeContext.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e9c1d6726ee54debb5debd3adb441e86b3adf5e4
--- /dev/null
+++ b/src/app/providers/ThemeProviders/lib/ThemeContext.ts
@@ -0,0 +1,15 @@
+import { createContext } from 'react';
+
+export enum Theme {
+ LIGHT = 'light',
+ DARK = 'dark',
+}
+
+export interface ThemeContextProps {
+ theme?: Theme;
+ setTheme?: (theme: Theme) => void;
+}
+
+export const ThemeContext = createContext({});
+
+export const LOCAL_STORAGE_THEME_KEY = 'themeStart';
diff --git a/src/app/providers/ThemeProviders/lib/useTheme.ts b/src/app/providers/ThemeProviders/lib/useTheme.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a4f0d36ec01acfac7b2a2dfb4bb326361edcedc9
--- /dev/null
+++ b/src/app/providers/ThemeProviders/lib/useTheme.ts
@@ -0,0 +1,35 @@
+import { useContext } from 'react';
+import { LOCAL_STORAGE_THEME_KEY, Theme, ThemeContext } from './ThemeContext';
+
+interface UseThemeResult {
+ toggleTheme: (newTheme: Theme) => void;
+ theme: Theme;
+}
+
+export function useTheme(): UseThemeResult {
+ const { theme, setTheme } = useContext(ThemeContext);
+
+ const toggleTheme = (newTheme: Theme) => {
+ setTheme?.(newTheme);
+
+ // Что бы не перетереть доп классы на body меняем только сам класс темы, а остальные оставляем как есть
+ let bodyClassesArray: any = document.body.className.split(' ');
+
+ if (bodyClassesArray.includes('light')) {
+ bodyClassesArray.splice(bodyClassesArray.indexOf('light'), 1);
+ }
+
+ if (bodyClassesArray.includes('dark')) {
+ bodyClassesArray.splice(bodyClassesArray.indexOf('dark'), 1);
+ }
+
+ bodyClassesArray = bodyClassesArray.join(' ');
+ document.body.className = `${bodyClassesArray} ${newTheme || Theme.LIGHT}`;
+ localStorage.setItem(LOCAL_STORAGE_THEME_KEY, newTheme);
+ };
+
+ return {
+ theme: theme || Theme.LIGHT,
+ toggleTheme,
+ };
+}
diff --git a/src/app/providers/ThemeProviders/ui/ThemeProvider.tsx b/src/app/providers/ThemeProviders/ui/ThemeProvider.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a64b450548b9a965d4746c547c0e2eb1721e6006
--- /dev/null
+++ b/src/app/providers/ThemeProviders/ui/ThemeProvider.tsx
@@ -0,0 +1,24 @@
+import { FC, ReactNode, useMemo, useState } from 'react';
+import { LOCAL_STORAGE_THEME_KEY, Theme, ThemeContext } from '../lib/ThemeContext';
+
+const defaultTheme = (localStorage.getItem(LOCAL_STORAGE_THEME_KEY) as Theme) || Theme.LIGHT;
+
+interface Props {
+ children: ReactNode;
+}
+
+const ThemeProvider: FC = ({ children }) => {
+ const [theme, setTheme] = useState(defaultTheme);
+
+ const defaultProps = useMemo(
+ () => ({
+ theme,
+ setTheme,
+ }),
+ [theme]
+ );
+
+ return {children};
+};
+
+export default ThemeProvider;
diff --git a/src/app/types/global.d.ts b/src/app/types/global.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e15bf357664046431916fce57a5adcf23440fcee
--- /dev/null
+++ b/src/app/types/global.d.ts
@@ -0,0 +1,18 @@
+declare module '*.scss' {
+ interface IClassNames {
+ [className: string]: string;
+ }
+ const classnames: IClassNames;
+ export = classnames;
+}
+
+declare module 'inputmask';
+declare module '*.png';
+declare module '*.jpg';
+declare module '*.jpeg';
+declare module '*.svg' {
+ import React from 'react';
+
+ const SVG: React.VFC>;
+ export default SVG;
+}
diff --git a/src/entities/Post/api/fetchPostById.ts b/src/entities/Post/api/fetchPostById.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5599fff3628790b19dea72841452345784d80865
--- /dev/null
+++ b/src/entities/Post/api/fetchPostById.ts
@@ -0,0 +1,11 @@
+import { PostType } from '../model/types/post';
+import { $api } from '@/shared/api/axiosInstance';
+
+type FetchPostByIdProps = number;
+type FetchPostByIdResponse = PostType;
+
+export const fetchPostById = async (postId: FetchPostByIdProps) => {
+ const { data } = await $api.get(`/posts/${postId}`);
+
+ return data;
+};
diff --git a/src/entities/Post/api/fetchPosts.ts b/src/entities/Post/api/fetchPosts.ts
new file mode 100644
index 0000000000000000000000000000000000000000..19ccb326c60a892d29602f23afa0570e38b5e728
--- /dev/null
+++ b/src/entities/Post/api/fetchPosts.ts
@@ -0,0 +1,10 @@
+import { PostType } from '../model/types/post';
+import { $api } from '@/shared/api/axiosInstance';
+
+type FetchPostsResponse = PostType[];
+
+export const fetchPosts = async () => {
+ const { data } = await $api.get(`/posts?limit=5`);
+
+ return data;
+};
diff --git a/src/entities/Post/index.ts b/src/entities/Post/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..375ac629ce7403e600a5f3bf8535b99cddda1af6
--- /dev/null
+++ b/src/entities/Post/index.ts
@@ -0,0 +1,4 @@
+export { PostCard } from './ui/PostCard/PostCard';
+export type { PostType } from './model/types/post';
+export { useFetchPosts } from './lib/query/useFetchPosts';
+export { useFetchPostById } from './lib/query/useFetchPostById';
diff --git a/src/entities/Post/lib/query/useFetchPostById.tsx b/src/entities/Post/lib/query/useFetchPostById.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..7fdf4e64bc2fd658d17bcd920636ceb8667224d0
--- /dev/null
+++ b/src/entities/Post/lib/query/useFetchPostById.tsx
@@ -0,0 +1,10 @@
+import { useQuery } from '@tanstack/react-query';
+import { fetchPostById } from '../../api/fetchPostById';
+
+export const useFetchPostById = (postId: number | undefined) => {
+ return useQuery({
+ queryKey: ['fetchPostById', postId],
+ queryFn: () => fetchPostById(postId || 1),
+ enabled: !!postId,
+ });
+};
diff --git a/src/entities/Post/lib/query/useFetchPosts.tsx b/src/entities/Post/lib/query/useFetchPosts.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..7e3d8b36298a414226dc4d34b034601bbe90fe3f
--- /dev/null
+++ b/src/entities/Post/lib/query/useFetchPosts.tsx
@@ -0,0 +1,9 @@
+import { useQuery } from '@tanstack/react-query';
+import { fetchPosts } from '../../api/fetchPosts';
+
+export const useFetchPosts = () => {
+ return useQuery({
+ queryKey: ['fetchPosts'],
+ queryFn: () => fetchPosts(),
+ });
+};
diff --git a/src/entities/Post/model/types/post.ts b/src/entities/Post/model/types/post.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d7b7364571bd032e557dc79beb100602773e07c8
--- /dev/null
+++ b/src/entities/Post/model/types/post.ts
@@ -0,0 +1,6 @@
+export interface PostType {
+ id: number;
+ title: string;
+ body: string;
+ userId: number;
+}
diff --git a/src/entities/Post/model/types/postSchema.ts b/src/entities/Post/model/types/postSchema.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/src/entities/Post/ui/PostCard/PostCard.module.scss b/src/entities/Post/ui/PostCard/PostCard.module.scss
new file mode 100644
index 0000000000000000000000000000000000000000..73ed63ab707938b613589790a8e5b73b651352b9
--- /dev/null
+++ b/src/entities/Post/ui/PostCard/PostCard.module.scss
@@ -0,0 +1,20 @@
+@import '@/app/globalStyles/config/mixin.scss';
+
+.PostCard {
+ display: block;
+}
+
+.content {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px;
+ border-radius: 8px;
+ border: 1px solid var(--s-accent-c);
+}
+
+.buttons {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
diff --git a/src/entities/Post/ui/PostCard/PostCard.tsx b/src/entities/Post/ui/PostCard/PostCard.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a14a3a36dcac2f5f7849797300f97a497d00d675
--- /dev/null
+++ b/src/entities/Post/ui/PostCard/PostCard.tsx
@@ -0,0 +1,27 @@
+import { ReactNode } from 'react';
+import { classNames } from '@/shared/lib/classNames/classNames';
+import { PostType } from '../../model/types/post';
+import cls from './PostCard.module.scss';
+
+interface PostCardProps {
+ className?: string;
+ post: PostType;
+ editButton?: ReactNode;
+ deleteButton?: ReactNode;
+}
+
+export const PostCard = (props: PostCardProps) => {
+ const { className, post, editButton, deleteButton } = props;
+
+ return (
+
+
+
{post.title}
+
+ {editButton}
+ {deleteButton}
+
+
+
+ );
+};
diff --git a/src/entities/User/index.ts b/src/entities/User/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2247911c8f34ab8b23b90261d3fc52f5ed51a0ef
--- /dev/null
+++ b/src/entities/User/index.ts
@@ -0,0 +1 @@
+export type { UserType } from './model/types/userTypes';
diff --git a/src/entities/User/model/store/useUserStore.ts b/src/entities/User/model/store/useUserStore.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f6bfeae9bda3d0c6fc8fd737f209421eabe73085
--- /dev/null
+++ b/src/entities/User/model/store/useUserStore.ts
@@ -0,0 +1,10 @@
+import { create } from 'zustand';
+import { devtools } from 'zustand/middleware';
+import { UserSchema } from '../types/userSchema';
+
+export const useUserStore = create()(
+ devtools((set) => ({
+ authUser: undefined,
+ setAuthUser: (user) => set({ authUser: user }),
+ }))
+);
diff --git a/src/entities/User/model/types/userSchema.ts b/src/entities/User/model/types/userSchema.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bb75651e9bbf0fc637a287b596b80de3027433c3
--- /dev/null
+++ b/src/entities/User/model/types/userSchema.ts
@@ -0,0 +1,6 @@
+import { UserType } from '@/entities/User';
+
+export interface UserSchema {
+ authUser?: UserType;
+ setAuthUser: (user: UserType) => void;
+}
diff --git a/src/entities/User/model/types/userTypes.ts b/src/entities/User/model/types/userTypes.ts
new file mode 100644
index 0000000000000000000000000000000000000000..39e213675f4fa0e2741beded880e58608f28a233
--- /dev/null
+++ b/src/entities/User/model/types/userTypes.ts
@@ -0,0 +1,6 @@
+export interface UserType {
+ id: number;
+ name: string;
+ surname: string;
+ email: string;
+}
diff --git a/src/features/Auth/api/signInByEmail.ts b/src/features/Auth/api/signInByEmail.ts
new file mode 100644
index 0000000000000000000000000000000000000000..65f01801680cbceefac87c8232c6967345cf8db8
--- /dev/null
+++ b/src/features/Auth/api/signInByEmail.ts
@@ -0,0 +1,16 @@
+import { $api } from '@/shared/api/axiosInstance';
+import { UserType } from '@/entities/User';
+import { AuthByEmailType } from '../ui/AuthByEmail/AuthByEmail';
+
+type LoginProps = AuthByEmailType;
+type LoginResponse = {
+ status: number;
+ user: UserType;
+ token: string;
+};
+
+export const signInByEmail = async (authData: LoginProps) => {
+ const { data } = await $api.post(`/login`, authData);
+
+ return data;
+};
diff --git a/src/features/Auth/api/signOut.ts b/src/features/Auth/api/signOut.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1387de01b23d7a1c1b47ff9e87b151346533eb9b
--- /dev/null
+++ b/src/features/Auth/api/signOut.ts
@@ -0,0 +1,12 @@
+import { $api } from '@/shared/api/axiosInstance';
+
+type LogoutResponse = {
+ status: number;
+ message: string;
+};
+
+export const signOut = async () => {
+ const { data } = await $api.post(`/logout`);
+
+ return data;
+};
diff --git a/src/features/Auth/index.ts b/src/features/Auth/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d5b25d66f20ef67c01987b82458254c8c9cdda96
--- /dev/null
+++ b/src/features/Auth/index.ts
@@ -0,0 +1,4 @@
+export { useSignOut } from './lib/query/useSignOut';
+export { AuthByEmail } from './ui/AuthByEmail/AuthByEmail';
+export { LogoutButton } from './ui/LogoutButton/LogoutButton';
+export { useSignInByEmail } from './lib/query/useSignInByEmail';
diff --git a/src/features/Auth/lib/query/useSignInByEmail.tsx b/src/features/Auth/lib/query/useSignInByEmail.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..4233de67ed08937bd00f53f5776c52c67d87133a
--- /dev/null
+++ b/src/features/Auth/lib/query/useSignInByEmail.tsx
@@ -0,0 +1,17 @@
+import { useMutation } from '@tanstack/react-query';
+import { useNavigate } from 'react-router-dom';
+import { signInByEmail } from '../../api/signInByEmail';
+import { USER_ACCESS_TOKEN_KEY, USER_AUTH_DATA } from '@/shared/const/localStorage';
+
+export const useSignInByEmail = () => {
+ const navigate = useNavigate();
+
+ return useMutation({
+ mutationFn: signInByEmail,
+ onSuccess: (data) => {
+ localStorage.setItem(USER_AUTH_DATA, JSON.stringify(data.user));
+ localStorage.setItem(USER_ACCESS_TOKEN_KEY, JSON.stringify(data.token));
+ navigate('/');
+ },
+ });
+};
diff --git a/src/features/Auth/lib/query/useSignOut.tsx b/src/features/Auth/lib/query/useSignOut.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..bc29a61fe2ea96e7b12c99aab110ea463f77a949
--- /dev/null
+++ b/src/features/Auth/lib/query/useSignOut.tsx
@@ -0,0 +1,17 @@
+import { useMutation } from '@tanstack/react-query';
+import { useNavigate } from 'react-router-dom';
+import { signOut } from '../../api/signOut';
+import { USER_ACCESS_TOKEN_KEY, USER_AUTH_DATA } from '@/shared/const/localStorage';
+
+export const useSignOut = () => {
+ const navigate = useNavigate();
+
+ return useMutation({
+ mutationFn: signOut,
+ onSuccess: () => {
+ localStorage.removeItem(USER_AUTH_DATA);
+ localStorage.removeItem(USER_ACCESS_TOKEN_KEY);
+ navigate('/login');
+ },
+ });
+};
diff --git a/src/features/Auth/ui/AuthByEmail/AuthByEmail.module.scss b/src/features/Auth/ui/AuthByEmail/AuthByEmail.module.scss
new file mode 100644
index 0000000000000000000000000000000000000000..a9bd82828133e4972ea6dbb250e32f686cf82e4f
--- /dev/null
+++ b/src/features/Auth/ui/AuthByEmail/AuthByEmail.module.scss
@@ -0,0 +1,9 @@
+@import '@/app/globalStyles/config/mixin.scss';
+
+.AuthByEmail {
+ display: block;
+}
+
+.field {
+ margin-bottom: 12px;
+}
diff --git a/src/features/Auth/ui/AuthByEmail/AuthByEmail.tsx b/src/features/Auth/ui/AuthByEmail/AuthByEmail.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..50dbbd0979ff9aaf80985c427a338a35e65129b1
--- /dev/null
+++ b/src/features/Auth/ui/AuthByEmail/AuthByEmail.tsx
@@ -0,0 +1,69 @@
+import { z } from 'zod';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { FormProvider, useForm } from 'react-hook-form';
+import { HInput } from '@/shared/ui/FormComponents';
+import { classNames } from '@/shared/lib/classNames/classNames';
+import { Button, ButtonSize, ButtonTheme } from '@/shared/ui/Button';
+import { useSignInByEmail } from '../../lib/query/useSignInByEmail';
+import cls from './AuthByEmail.module.scss';
+
+const AuthByEmailSchema = z.object({
+ email: z.string().email('email написан некоректно'),
+ password: z.string().min(5, { message: 'Введите не менее 5 символов' }),
+});
+
+export type AuthByEmailType = z.infer;
+
+interface AuthByEmailProps {
+ className?: string;
+}
+
+const defaultValues = {
+ email: '',
+ password: '',
+};
+
+export const AuthByEmail = (props: AuthByEmailProps) => {
+ const { className } = props;
+ const { mutate: onAuth, isPending } = useSignInByEmail();
+
+ const methods = useForm({
+ defaultValues,
+ resolver: zodResolver(AuthByEmailSchema),
+ });
+
+ const { handleSubmit } = methods;
+
+ const submitHandler = async (loginData: AuthByEmailType) => {
+ onAuth(loginData);
+ };
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/src/features/Auth/ui/LogoutButton/LogoutButton.module.scss b/src/features/Auth/ui/LogoutButton/LogoutButton.module.scss
new file mode 100644
index 0000000000000000000000000000000000000000..c8aa6aa43524b85efa87e5a9e3a28fc5b77aca07
--- /dev/null
+++ b/src/features/Auth/ui/LogoutButton/LogoutButton.module.scss
@@ -0,0 +1,5 @@
+@import '@/app/globalStyles/config/mixin.scss';
+
+.LogoutButton {
+ display: block;
+}
diff --git a/src/features/Auth/ui/LogoutButton/LogoutButton.tsx b/src/features/Auth/ui/LogoutButton/LogoutButton.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..2481b0fd18eddd45ea56b7e702b78df2df8f0d1e
--- /dev/null
+++ b/src/features/Auth/ui/LogoutButton/LogoutButton.tsx
@@ -0,0 +1,23 @@
+import { useSignOut } from '@/features/Auth';
+import { Button, ButtonTheme } from '@/shared/ui/Button';
+import { classNames } from '@/shared/lib/classNames/classNames';
+import cls from './LogoutButton.module.scss';
+
+interface LogoutButtonProps {
+ className?: string;
+}
+
+export const LogoutButton = (props: LogoutButtonProps) => {
+ const { className } = props;
+ const { mutate: onLogout } = useSignOut();
+
+ return (
+
+ );
+};
diff --git a/src/features/Menu/index.ts b/src/features/Menu/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d959ab662a5d94819cad777a4a1c683811e523a9
--- /dev/null
+++ b/src/features/Menu/index.ts
@@ -0,0 +1 @@
+export { Menu } from './ui/Menu';
diff --git a/src/features/Menu/ui/Menu.module.scss b/src/features/Menu/ui/Menu.module.scss
new file mode 100644
index 0000000000000000000000000000000000000000..5c46170cfb56ce7642b49410fae7693f461695f2
--- /dev/null
+++ b/src/features/Menu/ui/Menu.module.scss
@@ -0,0 +1,10 @@
+@import '@/app/globalStyles/config/mixin.scss';
+
+.Menu {
+ display: block;
+}
+
+.list {
+ display: flex;
+ gap: 24px;
+}
diff --git a/src/features/Menu/ui/Menu.tsx b/src/features/Menu/ui/Menu.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..1f75c5c23e4fd4ae30039c31411cfe6304f01aec
--- /dev/null
+++ b/src/features/Menu/ui/Menu.tsx
@@ -0,0 +1,38 @@
+import { classNames } from '@/shared/lib/classNames/classNames';
+import cls from './Menu.module.scss';
+import { AppLink, AppLinkTheme } from '@/shared/ui/AppLink';
+import Location from '@/shared/assets/icons/location.svg?react';
+
+interface MenuProps {
+ className?: string;
+}
+
+export const Menu = (props: MenuProps) => {
+ const { className } = props;
+
+ return (
+
+ );
+};
diff --git a/src/features/Post/api/createPost.ts b/src/features/Post/api/createPost.ts
new file mode 100644
index 0000000000000000000000000000000000000000..08d4a124b96e30f22c717eec216dc69baa15879c
--- /dev/null
+++ b/src/features/Post/api/createPost.ts
@@ -0,0 +1,19 @@
+import { $api } from '@/shared/api/axiosInstance';
+import { PostType } from '@/entities/Post';
+import { PostFormType } from '../ui/PostForm/PostForm';
+
+type CreatePostProps = {
+ user_id: number;
+} & PostFormType;
+
+type CreatePostResponse = {
+ status: number;
+ message: string;
+ post: PostType;
+};
+
+export const createPost = async (props: CreatePostProps) => {
+ const { data } = await $api.post(`/posts/create`, props);
+
+ return data;
+};
diff --git a/src/features/Post/api/deletePost.ts b/src/features/Post/api/deletePost.ts
new file mode 100644
index 0000000000000000000000000000000000000000..43acf89f7ee822f34ff25894901f357c06e771e3
--- /dev/null
+++ b/src/features/Post/api/deletePost.ts
@@ -0,0 +1,15 @@
+import { $api } from '@/shared/api/axiosInstance';
+
+type DeletePostProps = {
+ post_id: number;
+};
+type DeletePostResponse = {
+ status: number;
+ message: string;
+};
+
+export const deletePost = async (props: DeletePostProps) => {
+ const { data } = await $api.post(`/posts/delete`, props);
+
+ return data;
+};
diff --git a/src/features/Post/api/updatePost.ts b/src/features/Post/api/updatePost.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b5ed3d9c0c852348352291f816362ccbc1654933
--- /dev/null
+++ b/src/features/Post/api/updatePost.ts
@@ -0,0 +1,18 @@
+import { $api } from '@/shared/api/axiosInstance';
+import { PostFormType } from '../ui/PostForm/PostForm';
+
+type UpdatePostProps = {
+ post_id: number;
+ user_id: number;
+} & PostFormType;
+
+type UpdatePostResponse = {
+ status: number;
+ message: string;
+};
+
+export const updatePost = async (props: UpdatePostProps) => {
+ const { data } = await $api.post(`/posts/update`, props);
+
+ return data;
+};
diff --git a/src/features/Post/index.ts b/src/features/Post/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..69c1461a83aafacb906285ca935627bb40bdfee4
--- /dev/null
+++ b/src/features/Post/index.ts
@@ -0,0 +1,4 @@
+export { PostForm } from './ui/PostForm/PostForm';
+export { EditPost } from './ui/EditPost/EditPost';
+export { usePostStore } from './model/store/usePostStore';
+export { DeletePost } from './ui/DeletePost/DeletePost';
diff --git a/src/features/Post/lib/query/useCreatePost.tsx b/src/features/Post/lib/query/useCreatePost.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..2baf16e21939e3c502212870aba4f0d9ef467912
--- /dev/null
+++ b/src/features/Post/lib/query/useCreatePost.tsx
@@ -0,0 +1,13 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { createPost } from '../../api/createPost';
+
+export const useCreatePost = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: createPost,
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['fetchPosts'] });
+ },
+ });
+};
diff --git a/src/features/Post/lib/query/useDeletePost.tsx b/src/features/Post/lib/query/useDeletePost.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..dbd1e32d7c35fcfbcc469c93ee2b82294fe531fa
--- /dev/null
+++ b/src/features/Post/lib/query/useDeletePost.tsx
@@ -0,0 +1,13 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { deletePost } from '../../api/deletePost';
+
+export const useDeletePost = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: deletePost,
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['fetchPosts'] });
+ },
+ });
+};
diff --git a/src/features/Post/lib/query/useUpdatePost.tsx b/src/features/Post/lib/query/useUpdatePost.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..6f6e7156b3016fd42f105ac2c19915c0ceea2ab5
--- /dev/null
+++ b/src/features/Post/lib/query/useUpdatePost.tsx
@@ -0,0 +1,13 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { updatePost } from '../../api/updatePost';
+
+export const useUpdatePost = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: updatePost,
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['fetchPosts'] });
+ },
+ });
+};
diff --git a/src/features/Post/model/store/usePostStore.ts b/src/features/Post/model/store/usePostStore.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b513cd4fae6053445376f31a39b16a6d190578d6
--- /dev/null
+++ b/src/features/Post/model/store/usePostStore.ts
@@ -0,0 +1,12 @@
+import { create } from 'zustand';
+import { devtools } from 'zustand/middleware';
+import { PostSchema } from '../types/postSchema';
+
+export const usePostStore = create()(
+ devtools((set, get) => ({
+ isModalActive: false,
+ editablePost: undefined,
+ toggleModal: () => set({ isModalActive: !get().isModalActive }),
+ changingEditablePost: (postId) => set({ editablePostId: postId }),
+ }))
+);
diff --git a/src/features/Post/model/types/postSchema.ts b/src/features/Post/model/types/postSchema.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9485cc45cac9d5697c03b4a46aa5da8687b49376
--- /dev/null
+++ b/src/features/Post/model/types/postSchema.ts
@@ -0,0 +1,6 @@
+export interface PostSchema {
+ isModalActive: boolean;
+ editablePostId?: number;
+ toggleModal: () => void;
+ changingEditablePost: (postId?: number) => void;
+}
diff --git a/src/features/Post/ui/CreatePost/CreatePost.module.scss b/src/features/Post/ui/CreatePost/CreatePost.module.scss
new file mode 100644
index 0000000000000000000000000000000000000000..ddde3c41c3b01ac6575f0bf4fc6f468239a42dc3
--- /dev/null
+++ b/src/features/Post/ui/CreatePost/CreatePost.module.scss
@@ -0,0 +1,5 @@
+@import '@/app/globalStyles/config/mixin.scss';
+
+.CreatePost {
+ margin-bottom: 48px;
+}
diff --git a/src/features/Post/ui/CreatePost/CreatePost.tsx b/src/features/Post/ui/CreatePost/CreatePost.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..59ab1dfaec60d953801230176991a874f37126bc
--- /dev/null
+++ b/src/features/Post/ui/CreatePost/CreatePost.tsx
@@ -0,0 +1,30 @@
+import { classNames } from '@/shared/lib/classNames/classNames';
+import cls from './CreatePost.module.scss';
+import { usePostStore } from '@/features/Post';
+import { Button, ButtonSize, ButtonTheme } from '@/shared/ui/Button';
+
+interface CreatePostProps {
+ className?: string;
+}
+
+export const CreatePost = (props: CreatePostProps) => {
+ const { className } = props;
+ const toggleModal = usePostStore((state) => state.toggleModal);
+ const changingEditablePost = usePostStore((state) => state.changingEditablePost);
+
+ const openEditPostForm = () => {
+ changingEditablePost(undefined);
+ toggleModal();
+ };
+
+ return (
+
+ );
+};
diff --git a/src/features/Post/ui/DeletePost/DeletePost.module.scss b/src/features/Post/ui/DeletePost/DeletePost.module.scss
new file mode 100644
index 0000000000000000000000000000000000000000..678b3c33b853f1f2b659034737ea395ed6107472
--- /dev/null
+++ b/src/features/Post/ui/DeletePost/DeletePost.module.scss
@@ -0,0 +1,5 @@
+@import '@/app/globalStyles/config/mixin.scss';
+
+.DeleteButton {
+ display: block;
+}
diff --git a/src/features/Post/ui/DeletePost/DeletePost.tsx b/src/features/Post/ui/DeletePost/DeletePost.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..15738d7d630265d1bedda0e14676fb9bada56fce
--- /dev/null
+++ b/src/features/Post/ui/DeletePost/DeletePost.tsx
@@ -0,0 +1,25 @@
+import { classNames } from '@/shared/lib/classNames/classNames';
+import { Button, ButtonSize, ButtonTheme } from '@/shared/ui/Button';
+import { useDeletePost } from '../../lib/query/useDeletePost';
+import cls from './DeletePost.module.scss';
+
+interface DeletePostProps {
+ className?: string;
+ postId: number;
+}
+
+export const DeletePost = (props: DeletePostProps) => {
+ const { className, postId } = props;
+ const { mutate: onDelete } = useDeletePost();
+
+ return (
+
+ );
+};
diff --git a/src/features/Post/ui/EditPost/EditPost.module.scss b/src/features/Post/ui/EditPost/EditPost.module.scss
new file mode 100644
index 0000000000000000000000000000000000000000..f0e546770227c96b3a28936930c79341b1dccef7
--- /dev/null
+++ b/src/features/Post/ui/EditPost/EditPost.module.scss
@@ -0,0 +1,5 @@
+@import '@/app/globalStyles/config/mixin.scss';
+
+.EditButton {
+ display: block;
+}
diff --git a/src/features/Post/ui/EditPost/EditPost.tsx b/src/features/Post/ui/EditPost/EditPost.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..85b8b00ff826692659b3d70d7e560aca7a4105a9
--- /dev/null
+++ b/src/features/Post/ui/EditPost/EditPost.tsx
@@ -0,0 +1,31 @@
+import { classNames } from '@/shared/lib/classNames/classNames';
+import { Button, ButtonSize, ButtonTheme } from '@/shared/ui/Button';
+import { usePostStore } from '../../model/store/usePostStore';
+import cls from './EditPost.module.scss';
+
+interface EditPostProps {
+ className?: string;
+ postId: number;
+}
+
+export const EditPost = (props: EditPostProps) => {
+ const { className, postId } = props;
+ const toggleModal = usePostStore((state) => state.toggleModal);
+ const changingEditablePost = usePostStore((state) => state.changingEditablePost);
+
+ const openEditPostForm = (id: number) => {
+ changingEditablePost(id);
+ toggleModal();
+ };
+
+ return (
+
+ );
+};
diff --git a/src/features/Post/ui/PostForm/PostForm.module.scss b/src/features/Post/ui/PostForm/PostForm.module.scss
new file mode 100644
index 0000000000000000000000000000000000000000..30e9c3840c70ce8bf7a2f4c70b5fa1c3b057a04e
--- /dev/null
+++ b/src/features/Post/ui/PostForm/PostForm.module.scss
@@ -0,0 +1,9 @@
+@import '@/app/globalStyles/config/mixin.scss';
+
+.PostForm {
+ display: block;
+}
+
+.field {
+ margin-bottom: 12px;
+}
diff --git a/src/features/Post/ui/PostForm/PostForm.stories.tsx b/src/features/Post/ui/PostForm/PostForm.stories.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..b859e0113153f0017a5f17c4b7640fcdb56fdda7
--- /dev/null
+++ b/src/features/Post/ui/PostForm/PostForm.stories.tsx
@@ -0,0 +1,18 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { PostForm } from './PostForm';
+
+const meta = {
+ title: 'Features/PostForm',
+ component: PostForm,
+ parameters: {
+ layout: 'centered',
+ },
+ tags: ['autodocs'],
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Primary: Story = {
+ args: {},
+};
diff --git a/src/features/Post/ui/PostForm/PostForm.tsx b/src/features/Post/ui/PostForm/PostForm.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..46fcc6fa4d57fe45eed6f9e5be47c39cb4741981
--- /dev/null
+++ b/src/features/Post/ui/PostForm/PostForm.tsx
@@ -0,0 +1,100 @@
+import { z } from 'zod';
+import { useEffect } from 'react';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { FormProvider, useForm } from 'react-hook-form';
+import { useFetchPostById } from '@/entities/Post';
+import { HInput } from '@/shared/ui/FormComponents';
+import { classNames } from '@/shared/lib/classNames/classNames';
+import { Button, ButtonSize, ButtonTheme } from '@/shared/ui/Button';
+import { usePostStore } from '../../model/store/usePostStore';
+import { useUpdatePost } from '../../lib/query/useUpdatePost';
+import { useCreatePost } from '../../lib/query/useCreatePost';
+import cls from './PostForm.module.scss';
+
+const PostFormSchema = z.object({
+ title: z //
+ .string()
+ .min(1, { message: 'Заполните поле' })
+ .max(255),
+ body: z //
+ .string()
+ .min(1, { message: 'Заполните поле' })
+ .max(255),
+});
+
+export type PostFormType = z.infer;
+
+interface PostFormProps {
+ className?: string;
+}
+
+const defaultValues = {
+ title: '',
+ body: '',
+};
+
+export const PostForm = (props: PostFormProps) => {
+ const { className } = props;
+ const editablePostId = usePostStore((state) => state.editablePostId);
+
+ const { data, isError, isLoading } = useFetchPostById(editablePostId);
+ const { mutate: create, isPending: isCreate } = useCreatePost();
+ const { mutate: update, isPending: isUpdate } = useUpdatePost();
+
+ const methods = useForm({
+ defaultValues,
+ resolver: zodResolver(PostFormSchema),
+ });
+
+ const { handleSubmit, reset } = methods;
+
+ useEffect(() => {
+ if (data) {
+ reset({
+ title: data.title,
+ body: data.body,
+ });
+ } else {
+ reset(defaultValues);
+ }
+ }, [data]);
+
+ const submitHandler = async (data: PostFormType) => {
+ if (editablePostId) {
+ update({ ...data, post_id: editablePostId!, user_id: 1 });
+ } else {
+ create({ ...data, user_id: 1 });
+ }
+ };
+
+ if (isLoading) return ...Загружаем данные
;
+ if (isError) return Что то пошло не так
;
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/src/features/SideMenu/index.ts b/src/features/SideMenu/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..10024751f4025492c720c711b206d182b6e296b6
--- /dev/null
+++ b/src/features/SideMenu/index.ts
@@ -0,0 +1,2 @@
+export { useSideMenuStore } from './model/store/useSideMenuStore';
+export { SideMenuButton } from './ui/SideMenuButton/SideMenuButton';
diff --git a/src/features/SideMenu/model/store/useSideMenuStore.ts b/src/features/SideMenu/model/store/useSideMenuStore.ts
new file mode 100644
index 0000000000000000000000000000000000000000..549943f7581e36e11aad7f151d357d891be2eed3
--- /dev/null
+++ b/src/features/SideMenu/model/store/useSideMenuStore.ts
@@ -0,0 +1,16 @@
+import { create } from 'zustand';
+import { devtools } from 'zustand/middleware';
+import { SideMenuSchema } from '../types/sideMenuSchema';
+
+export const useSideMenuStore = create()(
+ devtools((set, get) => ({
+ isSideMenuActive: false,
+ // toggleBurger: () =>
+ // set(state => {
+ // return { isBurgerActive: !state.isBurgerActive };
+ // }),
+ toggleSideMenu: () => {
+ set({ isSideMenuActive: !get().isSideMenuActive });
+ },
+ }))
+);
diff --git a/src/features/SideMenu/model/types/sideMenuSchema.ts b/src/features/SideMenu/model/types/sideMenuSchema.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f44e71f98272bbf1dc8c75812a8117be7971d906
--- /dev/null
+++ b/src/features/SideMenu/model/types/sideMenuSchema.ts
@@ -0,0 +1,4 @@
+export interface SideMenuSchema {
+ isSideMenuActive: boolean;
+ toggleSideMenu: () => void;
+}
diff --git a/src/features/SideMenu/ui/SideMenuButton/SideMenuButton.module.scss b/src/features/SideMenu/ui/SideMenuButton/SideMenuButton.module.scss
new file mode 100644
index 0000000000000000000000000000000000000000..66a7d4279781169d309a9867c5bb72b2306307ce
--- /dev/null
+++ b/src/features/SideMenu/ui/SideMenuButton/SideMenuButton.module.scss
@@ -0,0 +1,5 @@
+@import '@/app/globalStyles/config/mixin.scss';
+
+.SideMenuButton {
+ display: block;
+}
diff --git a/src/features/SideMenu/ui/SideMenuButton/SideMenuButton.tsx b/src/features/SideMenu/ui/SideMenuButton/SideMenuButton.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..765189d8659067c7376491b6698d653721931f97
--- /dev/null
+++ b/src/features/SideMenu/ui/SideMenuButton/SideMenuButton.tsx
@@ -0,0 +1,22 @@
+import { Burger } from '@/shared/ui/Burger';
+import { classNames } from '@/shared/lib/classNames/classNames';
+import { useSideMenuStore } from '../../model/store/useSideMenuStore';
+import cls from './SideMenuButton.module.scss';
+
+interface SideMenuButtonProps {
+ className?: string;
+}
+
+export const SideMenuButton = (props: SideMenuButtonProps) => {
+ const { className } = props;
+ const isSideMenuActive = useSideMenuStore((state) => state.isSideMenuActive);
+ const toggleSideMenu = useSideMenuStore((state) => state.toggleSideMenu);
+
+ return (
+
+ );
+};
diff --git a/src/features/TestFeatures/index.ts b/src/features/TestFeatures/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0a1fb3468fe7e1fb8351426671c1eb8e88c06c28
--- /dev/null
+++ b/src/features/TestFeatures/index.ts
@@ -0,0 +1 @@
+export { AllFieldForm } from './ui/AllFieldForm/AllFieldForm';
diff --git a/src/features/TestFeatures/ui/AllFieldForm/AllFieldForm.module.scss b/src/features/TestFeatures/ui/AllFieldForm/AllFieldForm.module.scss
new file mode 100644
index 0000000000000000000000000000000000000000..a91268766fca2ff332b330f7d9b8c9d3cb5ab880
--- /dev/null
+++ b/src/features/TestFeatures/ui/AllFieldForm/AllFieldForm.module.scss
@@ -0,0 +1,13 @@
+@import '@/app/globalStyles/config/mixin.scss';
+
+.AllFieldForm {
+ display: block;
+}
+
+.grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ align-items: center;
+ gap: 24px;
+ margin-bottom: 48px;
+}
diff --git a/src/features/TestFeatures/ui/AllFieldForm/AllFieldForm.stories.tsx b/src/features/TestFeatures/ui/AllFieldForm/AllFieldForm.stories.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..87760be46d45c6ab5d93d18f0e138febe0c9dcd2
--- /dev/null
+++ b/src/features/TestFeatures/ui/AllFieldForm/AllFieldForm.stories.tsx
@@ -0,0 +1,18 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { AllFieldForm } from './AllFieldForm';
+
+const meta = {
+ title: 'Features/AllFieldForm',
+ component: AllFieldForm,
+ parameters: {
+ layout: 'centered',
+ },
+ tags: ['autodocs'],
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Primary: Story = {
+ args: {},
+};
diff --git a/src/features/TestFeatures/ui/AllFieldForm/AllFieldForm.tsx b/src/features/TestFeatures/ui/AllFieldForm/AllFieldForm.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..eca77a69c46896f971abff9478d8be6cf1be5717
--- /dev/null
+++ b/src/features/TestFeatures/ui/AllFieldForm/AllFieldForm.tsx
@@ -0,0 +1,159 @@
+import { z } from 'zod';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { FormProvider, useForm } from 'react-hook-form';
+import { HInput, HRadioButtons } from '@/shared/ui/FormComponents';
+import { classNames } from '@/shared/lib/classNames/classNames';
+import { Button, ButtonSize, ButtonTheme } from '@/shared/ui/Button';
+import cls from './AllFieldForm.module.scss';
+import { regexMap } from '@/shared/lib/utils/regexMap';
+import { IRadioButtons, RadioButtonsTheme } from '@/shared/ui/FormComponents/fieldsUI/RadioButtons/RadioButtons';
+import { HCheckbox } from '@/shared/ui/FormComponents/rhfFields/HCheckbox/HCheckbox';
+import { HTextarea } from '@/shared/ui/FormComponents/rhfFields/HTextarea/HTextarea';
+
+const AllFieldFormSchema = z.object({
+ textInput: z //
+ .string()
+ .min(1, { message: 'Заполните поле' })
+ .max(255),
+ maskIInput: z //
+ .string()
+ // .min(1, { message: 'Заполните поле' })
+ .max(255)
+ .regex(/^\+7 \(\d{3}\) \d{3}-\d{2}-\d{2}$/, { message: 'Укажите верный формат телефона' }),
+ maskIInputLatin: z //
+ .string()
+ // .min(1, { message: 'Заполните поле' })
+ .max(255),
+ radioButton: z //
+ .string()
+ // .min(1, { message: 'Выберите значение' })
+ .max(255),
+ radioButtonParam: z //
+ .string()
+ // .min(1, { message: 'Выберите значение' })
+ .max(255),
+ textarea: z //
+ .string()
+ .min(1, { message: 'Заполните поле' })
+ .max(255),
+ checkbox: z.string(),
+});
+
+type AllFieldFormType = z.infer;
+
+interface AllFieldFormProps {
+ className?: string;
+}
+
+const defaultValues = {
+ textInput: '',
+ maskIInput: '',
+ maskIInputLatin: '',
+ radioButton: '',
+ radioButtonParam: '',
+ checkbox: '',
+ textarea: '',
+};
+
+export const AllFieldForm = (props: AllFieldFormProps) => {
+ const { className } = props;
+ const regex = regexMap.onlyRuSymbols;
+
+ const methods = useForm({
+ defaultValues,
+ resolver: zodResolver(AllFieldFormSchema),
+ });
+
+ const { handleSubmit } = methods;
+
+ const submitHandler = async (data: AllFieldFormType) => {
+ console.log('data: ', data);
+ };
+
+ const radioData: IRadioButtons[] = [
+ {
+ labelName: 'Раз в квартал',
+ value: '0',
+ },
+ {
+ labelName: 'Раз в пол года',
+ value: '1',
+ },
+ {
+ labelName: 'Раз в год',
+ value: '2',
+ },
+ ];
+
+ const radioDataParam: IRadioButtons[] = [
+ {
+ labelName: 'Раз в квартал',
+ value: '0',
+ },
+ {
+ labelName: 'Раз в пол года',
+ value: '1',
+ },
+ {
+ labelName: 'Раз в год',
+ value: '2',
+ },
+ ];
+
+ return (
+
+ );
+};
diff --git a/src/features/TestFeatures/ui/AllFieldForm/AllFieldFormData.ts b/src/features/TestFeatures/ui/AllFieldForm/AllFieldFormData.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4f1879bee6b7ae9575520f94952a399a21763e73
--- /dev/null
+++ b/src/features/TestFeatures/ui/AllFieldForm/AllFieldFormData.ts
@@ -0,0 +1,46 @@
+import { z } from 'zod';
+import { IRadioButtons } from '@/shared/ui/FormComponents/fieldsUI/RadioButtons/RadioButtons';
+
+export const AllFieldFormSchema = z.object({
+ textInput: z //
+ .string()
+ .min(1, { message: 'Заполните поле' })
+ .max(255),
+ maskIInput: z //
+ .string()
+ .min(1, { message: 'Заполните поле' })
+ .max(255)
+ .regex(/^\+7 \(\d{3}\) \d{3}-\d{2}-\d{2}$/, { message: 'Укажите верный формат телефона' }),
+ maskIInputLatin: z //
+ .string()
+ .min(1, { message: 'Заполните поле' })
+ .max(255),
+ radioButton: z //
+ .string()
+ .min(1, { message: 'Заполните поле' })
+ .max(255),
+});
+
+export type AllFieldFormType = z.infer;
+
+export const defaultValues = {
+ textInput: '',
+ maskIInput: '',
+ maskIInputLatin: '',
+ radioButton: '',
+};
+
+export const allFieldFormRadioData: IRadioButtons[] = [
+ {
+ labelName: 'Раз в квартал',
+ value: '0',
+ },
+ {
+ labelName: 'Раз в пол года',
+ value: '1',
+ },
+ {
+ labelName: 'Раз в год',
+ value: '2',
+ },
+];
diff --git a/src/features/ThemeButton/index.ts b/src/features/ThemeButton/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e51bed3f7c4b8bd1e0b2e8126d66d28d34e7f722
--- /dev/null
+++ b/src/features/ThemeButton/index.ts
@@ -0,0 +1 @@
+export { ThemeButton } from './ui/ThemeButton';
diff --git a/src/features/ThemeButton/ui/ThemeButton.module.scss b/src/features/ThemeButton/ui/ThemeButton.module.scss
new file mode 100644
index 0000000000000000000000000000000000000000..88b328ecbb20ac090618b822803fa4288dd5e8fb
--- /dev/null
+++ b/src/features/ThemeButton/ui/ThemeButton.module.scss
@@ -0,0 +1,5 @@
+@import '@/app/globalStyles/config/mixin.scss';
+
+.ThemeButton {
+ display: block;
+}
diff --git a/src/features/ThemeButton/ui/ThemeButton.tsx b/src/features/ThemeButton/ui/ThemeButton.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..2e8df74a434b4ecd85ab61df60af8cf3af31179b
--- /dev/null
+++ b/src/features/ThemeButton/ui/ThemeButton.tsx
@@ -0,0 +1,23 @@
+import { classNames } from '@/shared/lib/classNames/classNames';
+import { Theme, useTheme } from '@/app/providers/ThemeProviders';
+import Sun from '@/shared/assets/icons/sun.svg?react';
+import Moon from '@/shared/assets/icons/moon.svg?react';
+import { Button } from '@/shared/ui/Button';
+import cls from './ThemeButton.module.scss';
+
+interface ThemeButtonProps {
+ className?: string;
+}
+
+export const ThemeButton = (props: ThemeButtonProps) => {
+ const { className } = props;
+ const { theme, toggleTheme } = useTheme();
+
+ return (
+