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 ( +
+

{{name}}

+
+ ); +}; 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 ( + + ); +}; diff --git a/src/shared/ui/Button/index.ts b/src/shared/ui/Button/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..89d7710aae327c4251351015532704acdccab81a --- /dev/null +++ b/src/shared/ui/Button/index.ts @@ -0,0 +1 @@ +export { Button, ButtonTheme, ButtonSize } from './Button'; diff --git a/src/shared/ui/FormComponents/fieldsUI/Cell/Cell.module.scss b/src/shared/ui/FormComponents/fieldsUI/Cell/Cell.module.scss new file mode 100644 index 0000000000000000000000000000000000000000..bcc0490c80c863e1e2e30f3659136d8d8c61aa03 --- /dev/null +++ b/src/shared/ui/FormComponents/fieldsUI/Cell/Cell.module.scss @@ -0,0 +1,44 @@ +@import '@/app/globalStyles/config/mixin.scss'; + +.Cell { + display: block; +} + +.content { + position: relative; + height: 100%; +} + +.name { + display: block; + margin-bottom: 8px; + font-size: 16px; + color: var(--t-primery-c); +} + +.data { + position: relative; + //padding: 19px; + background-color: var(--s-inverted-c); + border: 1px solid var(--s-secondary-c-7); + border-radius: 12px; + transition: 0.5s; + + @include before_768 { + padding: 12px 16px; + } +} + +.note { + margin-top: 4px; + font-size: 12px; + font-weight: 400; + color: var(--s-secondary-c-5); +} + +.errorMessage { + margin-top: 6px; + font-size: 14px; + line-height: 140%; + color: var(--error-primery-color); +} diff --git a/src/shared/ui/FormComponents/fieldsUI/Cell/Cell.tsx b/src/shared/ui/FormComponents/fieldsUI/Cell/Cell.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ad3393a0b58b9d6a80b1c5216a2accf6bb77fbe8 --- /dev/null +++ b/src/shared/ui/FormComponents/fieldsUI/Cell/Cell.tsx @@ -0,0 +1,30 @@ +import { ReactNode } from 'react'; +import { classNames } from '@/shared/lib/classNames/classNames'; +import cls from './Cell.module.scss'; + +interface CellProps { + className?: string; + label?: string; + withoutBorder?: boolean; + fieldError?: any; + noteText?: ReactNode; + children: ReactNode; +} + +export const Cell = (props: CellProps) => { + const { className, label, withoutBorder, fieldError, noteText, children } = props; + + return ( +
+ {!withoutBorder && ( +
+ +
{children}
+
+ )} + {noteText &&
{noteText}
} + {withoutBorder && children} + {fieldError &&
{fieldError.message || 'Заполните поле'}
} +
+ ); +}; diff --git a/src/shared/ui/FormComponents/fieldsUI/Checkbox/Checkbox.module.scss b/src/shared/ui/FormComponents/fieldsUI/Checkbox/Checkbox.module.scss new file mode 100644 index 0000000000000000000000000000000000000000..619f565e93f155bb056a18e4f39f74e87bfe7f23 --- /dev/null +++ b/src/shared/ui/FormComponents/fieldsUI/Checkbox/Checkbox.module.scss @@ -0,0 +1,59 @@ +@import '@/app/globalStyles/config/mixin.scss'; + +.Checkbox { + display: flex; + flex-shrink: 0; + align-items: center; +} + +.window { + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + margin-right: 12px; + background: var(--s-inverted-c); + border: 1px solid var(--s-primery-c); + border-radius: 1px; + transition: 0.5s; +} + +.iconMark { + opacity: 0; + fill: var(--s-primery-c); + transition: 0.5s; +} + +.name { + display: flex; + align-items: flex-start; + font-size: 16px; + font-weight: normal; + line-height: 22px; + color: var(--t-primery-c); + letter-spacing: 0.04em; + cursor: pointer; +} + +.content { + padding-top: 1px; +} + +.input:checked + .name { + .iconMark { + opacity: 1; + } +} + +.link { + color: var(--t-accent-c); + text-decoration: none; + border-bottom: 1px solid var(--s-primery-c); + transition: 0.5s; + + &:hover { + border-bottom: 1px solid transparent; + } +} diff --git a/src/shared/ui/FormComponents/fieldsUI/Checkbox/Checkbox.tsx b/src/shared/ui/FormComponents/fieldsUI/Checkbox/Checkbox.tsx new file mode 100644 index 0000000000000000000000000000000000000000..632e8e8c30462ec57c6f11a09ef4f5bf1f6c1bc7 --- /dev/null +++ b/src/shared/ui/FormComponents/fieldsUI/Checkbox/Checkbox.tsx @@ -0,0 +1,40 @@ +import { ReactNode, useEffect, useId, useRef } from 'react'; +import { classNames } from '@/shared/lib/classNames/classNames'; +import Checkmark from '@/shared/assets/icons/checkmark.svg?react'; +import cls from './Checkbox.module.scss'; + +interface CheckboxProps { + className?: string; + name: string; + children: ReactNode; + onChange?: (value: string) => void; + inputRef?: (ref: any) => void; +} +export const Checkbox = (props: CheckboxProps) => { + const { className, name, children, onChange, inputRef } = props; + const checkboxId = useId(); + const inputRefValue = useRef(null); + + useEffect(() => { + if (inputRef) inputRef(inputRefValue.current); + }, [inputRefValue.current]); + + return ( +
+ onChange?.(e.target.value)} + /> + +
+ ); +}; diff --git a/src/shared/ui/FormComponents/fieldsUI/Input/Input.module.scss b/src/shared/ui/FormComponents/fieldsUI/Input/Input.module.scss new file mode 100644 index 0000000000000000000000000000000000000000..6e6aec1b0f45bcd0aee5d70427200beeefb14f5c --- /dev/null +++ b/src/shared/ui/FormComponents/fieldsUI/Input/Input.module.scss @@ -0,0 +1,22 @@ +@import '@/app/globalStyles/config/mixin.scss'; + +.Input { + width: 100%; + padding: 19px; + font-size: 16px; + color: var(--t-primery-c); + background-color: transparent; + border: none; + outline: none; + + &::placeholder { + font-size: 16px; + line-height: 140%; + color: var(--t-descriptive-c-5); + letter-spacing: 0.04em; + + @include before-768 { + font-size: 16px; + } + } +} diff --git a/src/shared/ui/FormComponents/fieldsUI/Input/Input.tsx b/src/shared/ui/FormComponents/fieldsUI/Input/Input.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9cd31e9a2955af65af610056df1a87d0dd268e14 --- /dev/null +++ b/src/shared/ui/FormComponents/fieldsUI/Input/Input.tsx @@ -0,0 +1,36 @@ +import { InputHTMLAttributes, useEffect, useRef } from 'react'; +import Inputmask from 'inputmask'; +import { classNames } from '@/shared/lib/classNames/classNames'; +import cls from './Input.module.scss'; + +type HTMLInputProps = Omit, 'value' | 'onChange'>; + +interface InputProps extends HTMLInputProps { + className?: string; + value?: string; + onChange?: (value: string) => void; + inputRef?: (ref: any) => void; + mask?: string; + maskOptions?: any; +} + +export const Input = (props: InputProps) => { + const { className, value, onChange, inputRef, mask, maskOptions, ...otherProps } = props; + + const inputRefValue = useRef(null); + if (inputRefValue.current && (mask || maskOptions)) new Inputmask(mask, maskOptions).mask(inputRefValue.current); + + useEffect(() => { + if (inputRef) inputRef(inputRefValue.current); + }, [inputRefValue.current]); + + return ( + onChange?.(e.target.value)} + {...otherProps} + /> + ); +}; diff --git a/src/shared/ui/FormComponents/fieldsUI/RadioButtons/RadioButtons.module.scss b/src/shared/ui/FormComponents/fieldsUI/RadioButtons/RadioButtons.module.scss new file mode 100644 index 0000000000000000000000000000000000000000..d7f18efabdd94c2b25b3b9352de04850d5a11c57 --- /dev/null +++ b/src/shared/ui/FormComponents/fieldsUI/RadioButtons/RadioButtons.module.scss @@ -0,0 +1,104 @@ +@import '@/app/globalStyles/config/mixin.scss'; + +.RadioButtons { + display: block; +} + +.listTitle { + margin-bottom: 8px; + font-size: 16px; + color: var(--t-primery-c); +} + +.list { + display: flex; +} + +.item { + display: flex; + flex-shrink: 0; + align-items: center; + + &:not(:last-child) { + margin-right: 30px; + } +} + +.window { + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + margin-right: 12px; + background: transparent; + border: 1px solid var(--s-secondary-c-5); + border-radius: 50%; + transition: 0.3s; +} + +.circle { + width: 16px; + height: 16px; + background: var(--s-accent-c); + border-radius: 50%; + opacity: 0; + transition: 0.3s; +} + +.name { + display: flex; + align-items: center; + font-size: 16px; + font-weight: normal; + line-height: 24px; + color: #121212b2; + letter-spacing: 0.04em; + cursor: pointer; +} + +.input:checked + .name { + .circle { + opacity: 1; + } + + .window { + border-color: var(--s-accent-c); + } +} + +//Themes +.param { + display: inline-block; + + .list { + background-color: var(--s-secondary-c-3); + border: 1px solid var(--s-secondary-c-5); + border-radius: 8px; + overflow: hidden; + } + + .item { + &:not(:last-child) { + border-right: 1px solid var(--s-secondary-c-5); + margin-right: 0; + } + } + + .window { + display: none; + } + + .name { + padding: 18px 24px; + font-weight: 500; + background-color: var(--s-inverted-c); + transition: 0.3s; + } + + .input:checked + .name { + color: var(--s-inverted-c); + background-color: var(--s-accent-c); + } +} diff --git a/src/shared/ui/FormComponents/fieldsUI/RadioButtons/RadioButtons.tsx b/src/shared/ui/FormComponents/fieldsUI/RadioButtons/RadioButtons.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f7a49187406deecce250e3b7fbcb46a475ecd99d --- /dev/null +++ b/src/shared/ui/FormComponents/fieldsUI/RadioButtons/RadioButtons.tsx @@ -0,0 +1,51 @@ +import { classNames } from '@/shared/lib/classNames/classNames'; +import cls from './RadioButtons.module.scss'; + +export enum RadioButtonsTheme { + PARAM = 'param', +} +export interface IRadioButtons { + labelName: string; + value: string | number; +} + +interface RadioButtonsProps { + className?: string; + name: string; + title?: string; + data: IRadioButtons[]; + theme?: string; + currentValue?: string | number; + onChange?: (value: string) => void; +} + +export const RadioButtons = (props: RadioButtonsProps) => { + const { className, name, title, data, theme = '', currentValue, onChange } = props; + + return ( +
+ {title &&

{title}

} +
+ {data.map((radioButton, index) => ( +
+ onChange?.(e.target.value)} + /> + +
+ ))} +
+
+ ); +}; diff --git a/src/shared/ui/FormComponents/fieldsUI/Textarea/Textarea.module.scss b/src/shared/ui/FormComponents/fieldsUI/Textarea/Textarea.module.scss new file mode 100644 index 0000000000000000000000000000000000000000..479dbde273586a3daf34114e58914c888ce7a991 --- /dev/null +++ b/src/shared/ui/FormComponents/fieldsUI/Textarea/Textarea.module.scss @@ -0,0 +1,24 @@ +@import '@/app/globalStyles/config/mixin.scss'; + +.Textarea { + width: 100%; + min-height: 150px; + padding: 19px; + font-size: 16px; + color: var(--t-primery-c); + background-color: transparent; + border: none; + outline: none; + resize: none; + + &::placeholder { + font-size: 16px; + line-height: 140%; + color: var(--t-descriptive-c-5); + letter-spacing: 0.04em; + + @include before-768 { + font-size: 16px; + } + } +} diff --git a/src/shared/ui/FormComponents/fieldsUI/Textarea/Textarea.tsx b/src/shared/ui/FormComponents/fieldsUI/Textarea/Textarea.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6b8a6fdce144fa1fa6509ff881c8fe2ab2d9fdb4 --- /dev/null +++ b/src/shared/ui/FormComponents/fieldsUI/Textarea/Textarea.tsx @@ -0,0 +1,31 @@ +import { TextareaHTMLAttributes, useEffect, useRef } from 'react'; +import { classNames } from '@/shared/lib/classNames/classNames'; +import cls from './Textarea.module.scss'; + +type HTMLTextareaProps = Omit, 'value' | 'onChange'>; + +interface TextareaProps extends HTMLTextareaProps { + className?: string; + value?: string; + onChange?: (value: string) => void; + inputRef?: (ref: any) => void; +} + +export const Textarea = (props: TextareaProps) => { + const { className, value, onChange, inputRef, ...otherProps } = props; + const inputRefValue = useRef(null); + + useEffect(() => { + if (inputRef) inputRef(inputRefValue.current); + }, [inputRefValue.current]); + + return ( +