From 67388d9628ac4d3d98cd2c158f18891983a51e5e Mon Sep 17 00:00:00 2001 From: Valery Petrov Date: Wed, 20 May 2026 14:08:00 +0300 Subject: [PATCH] issues/27: generic content CRUD with field error widgets --- src/App.jsx | 62 ++--- src/api/apiArticle.js | 55 ---- src/api/apiContent.js | 117 ++++++++ src/api/apiDisease.js | 55 ---- src/api/apiMedicalCenter.js | 55 ---- src/api/apiNews.js | 55 ---- src/api/apiSitePromo.js | 55 ---- src/api/apiSiteServices.js | 55 ---- src/config/api.js | 7 + src/config/contentResources.js | 223 ++++++++++++++++ src/pages/AddArticlePage.jsx | 88 ------ src/pages/AddDiseasePage.jsx | 127 --------- src/pages/AddMedicalCenterPage.jsx | 148 ----------- src/pages/AddNewsPage.jsx | 103 ------- src/pages/AddSitePromoPage.jsx | 103 ------- src/pages/AddSiteServicesPage.jsx | 226 ---------------- src/pages/ArticleListPage.jsx | 124 --------- src/pages/DiseaseListPage.jsx | 115 -------- src/pages/EditArticlePage.jsx | 113 -------- src/pages/EditDiseasePage.jsx | 165 ------------ src/pages/EditMedicalCenterPage.jsx | 193 -------------- src/pages/EditNewsPage.jsx | 133 ---------- src/pages/EditSitePromoPage.jsx | 133 ---------- src/pages/EditSiteServicesPage.jsx | 297 --------------------- src/pages/MedicalCenterListPage.jsx | 115 -------- src/pages/NewsListPage.jsx | 117 -------- src/pages/SitePromoListPage.jsx | 117 -------- src/pages/SiteServicesListPage.jsx | 115 -------- src/pages/content/ContentEditPage.jsx | 369 ++++++++++++++++++++++++++ src/pages/content/ContentListPage.jsx | 210 +++++++++++++++ src/pages/content/index.jsx | 46 ++++ src/store/store.js | 7 +- src/styles/theme-override.scss | 28 ++ src/utils/parseSaveError.js | 123 +++++++++ 34 files changed, 1156 insertions(+), 2898 deletions(-) delete mode 100644 src/api/apiArticle.js create mode 100644 src/api/apiContent.js delete mode 100644 src/api/apiDisease.js delete mode 100644 src/api/apiMedicalCenter.js delete mode 100644 src/api/apiNews.js delete mode 100644 src/api/apiSitePromo.js delete mode 100644 src/api/apiSiteServices.js create mode 100644 src/config/api.js create mode 100644 src/config/contentResources.js delete mode 100644 src/pages/AddArticlePage.jsx delete mode 100644 src/pages/AddDiseasePage.jsx delete mode 100644 src/pages/AddMedicalCenterPage.jsx delete mode 100644 src/pages/AddNewsPage.jsx delete mode 100644 src/pages/AddSitePromoPage.jsx delete mode 100644 src/pages/AddSiteServicesPage.jsx delete mode 100644 src/pages/ArticleListPage.jsx delete mode 100644 src/pages/DiseaseListPage.jsx delete mode 100644 src/pages/EditArticlePage.jsx delete mode 100644 src/pages/EditDiseasePage.jsx delete mode 100644 src/pages/EditMedicalCenterPage.jsx delete mode 100644 src/pages/EditNewsPage.jsx delete mode 100644 src/pages/EditSitePromoPage.jsx delete mode 100644 src/pages/EditSiteServicesPage.jsx delete mode 100644 src/pages/MedicalCenterListPage.jsx delete mode 100644 src/pages/NewsListPage.jsx delete mode 100644 src/pages/SitePromoListPage.jsx delete mode 100644 src/pages/SiteServicesListPage.jsx create mode 100644 src/pages/content/ContentEditPage.jsx create mode 100644 src/pages/content/ContentListPage.jsx create mode 100644 src/pages/content/index.jsx create mode 100644 src/utils/parseSaveError.js diff --git a/src/App.jsx b/src/App.jsx index 445cff5..8b7cf4a 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -21,24 +21,26 @@ import { EditStockPage } from './pages/EditStockPage'; import { AddStockPage } from './pages/AddStockPage'; import { InfoclinicListPage } from './pages/InfoclinicListPage'; import { LostDoctorsPage } from './pages/LostDoctorsPage'; -import { NewsListPage } from './pages/NewsListPage'; -import { EditNewsPage } from './pages/EditNewsPage'; -import { AddNewsPage } from './pages/AddNewsPage'; -import { SitePromoListPage } from './pages/SitePromoListPage'; -import { EditSitePromoPage } from './pages/EditSitePromoPage'; -import { AddSitePromoPage } from './pages/AddSitePromoPage'; -import { DiseaseListPage } from './pages/DiseaseListPage'; -import { EditDiseasePage } from './pages/EditDiseasePage'; -import { AddDiseasePage } from './pages/AddDiseasePage'; -import { MedicalCenterListPage } from './pages/MedicalCenterListPage'; -import { EditMedicalCenterPage } from './pages/EditMedicalCenterPage'; -import { AddMedicalCenterPage } from './pages/AddMedicalCenterPage'; -import { ArticleListPage } from './pages/ArticleListPage'; -import { EditArticlePage } from './pages/EditArticlePage'; -import { AddArticlePage } from './pages/AddArticlePage'; -import { SiteServicesListPage } from './pages/SiteServicesListPage'; -import { EditSiteServicesPage } from './pages/EditSiteServicesPage'; -import { AddSiteServicesPage } from './pages/AddSiteServicesPage'; +import { + NewsListPage, + NewsEditPage, + NewsCreatePage, + SitePromoListPage, + SitePromoEditPage, + SitePromoCreatePage, + DiseaseListPage, + DiseaseEditPage, + DiseaseCreatePage, + MedicalCenterListPage, + MedicalCenterEditPage, + MedicalCenterCreatePage, + ArticleListPage, + ArticleEditPage, + ArticleCreatePage, + SiteServicesListPage, + SiteServicesEditPage, + SiteServicesCreatePage, +} from './pages/content'; function App() { return ( @@ -70,23 +72,23 @@ function App() { } /> } /> } /> - } /> - } /> + } /> + } /> } /> - } /> - } /> + } /> + } /> } /> - } /> - } /> + } /> + } /> } /> - } /> - } /> + } /> + } /> } /> - } /> - } /> + } /> + } /> } /> - } /> - } /> + } /> + } /> } /> } /> diff --git a/src/api/apiArticle.js b/src/api/apiArticle.js deleted file mode 100644 index 2a3b9fc..0000000 --- a/src/api/apiArticle.js +++ /dev/null @@ -1,55 +0,0 @@ -import { API, authHeader } from './apiSlice'; - -export const articleApi = API.injectEndpoints({ - endpoints: (build) => ({ - getArticleList: build.query({ - query: ({ search = '', page = '' }) => { - let queryString = '?'; - if (page) queryString += `page=${page}&limit=20`; - else queryString += `limit=20`; - if (search) queryString += `&search=${encodeURIComponent(search)}`; - return { - url: `/article/list${queryString}`, - }; - }, - refetchOnMountOrArgChange: true, - keepUnusedDataFor: 0, - }), - getArticle: build.query({ - query: ({ articleId }) => ({ - url: `/article/${articleId}`, - }), - }), - createArticle: build.mutation({ - query: ({ data }) => ({ - url: `/article/create`, - method: 'POST', - headers: authHeader(), - body: JSON.stringify(data), - }), - }), - updateArticle: build.mutation({ - query: ({ articleId, data }) => ({ - url: `/article/${articleId}`, - method: 'PUT', - headers: authHeader(), - body: JSON.stringify(data), - }), - }), - deleteArticle: build.mutation({ - query: ({ articleId }) => ({ - url: `/article/${articleId}`, - method: 'DELETE', - headers: authHeader(), - }), - }), - }), -}); - -export const { - useGetArticleQuery, - useGetArticleListQuery, - useCreateArticleMutation, - useUpdateArticleMutation, - useDeleteArticleMutation, -} = articleApi; diff --git a/src/api/apiContent.js b/src/api/apiContent.js new file mode 100644 index 0000000..271c9a7 --- /dev/null +++ b/src/api/apiContent.js @@ -0,0 +1,117 @@ +import { API, authHeader } from './apiSlice' +import { CONTENT_RESOURCES } from '../config/contentResources' + +const buildListQuery = (basePath, { usesLimit = false } = {}) => + ({ search = '', page = 1, perPage = 20 } = {}) => { + let queryString = `?page=${page}` + if (usesLimit) { + queryString += `&limit=${perPage}` + } else { + queryString += `&perPage=${perPage}` + } + if (search) { + queryString += `&search=${encodeURIComponent(search)}` + } + return { + url: `${basePath}/list${queryString}`, + } + } + +const injectResource = (resourceKey) => { + const { basePath, listUsesLimit } = CONTENT_RESOURCES[resourceKey] + const cap = resourceKey + .split('-') + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join('') + + return API.injectEndpoints({ + endpoints: (build) => ({ + [`get${cap}List`]: build.query({ + query: buildListQuery(basePath, { usesLimit: listUsesLimit }), + refetchOnMountOrArgChange: true, + keepUnusedDataFor: 0, + }), + [`get${cap}Item`]: build.query({ + query: (id) => ({ + url: `${basePath}/${id}`, + }), + }), + [`create${cap}`]: build.mutation({ + query: (data) => ({ + url: `${basePath}/create`, + method: 'POST', + headers: authHeader(), + body: JSON.stringify(data), + }), + }), + [`update${cap}`]: build.mutation({ + query: ({ id, data }) => ({ + url: `${basePath}/${id}`, + method: 'PUT', + headers: authHeader(), + body: JSON.stringify(data), + }), + }), + [`delete${cap}`]: build.mutation({ + query: (id) => ({ + url: `${basePath}/${id}`, + method: 'DELETE', + headers: authHeader(), + }), + }), + }), + overrideExisting: false, + }) +} + +const newsApi = injectResource('news') +const promoApi = injectResource('promo') +const diseaseApi = injectResource('disease') +const medicalCenterApi = injectResource('medical-center') +const articleApi = injectResource('article') +const siteServicesApi = injectResource('site-services') + +export const contentHooks = { + news: { + useListQuery: newsApi.useGetNewsListQuery, + useItemQuery: newsApi.useGetNewsItemQuery, + useCreateMutation: newsApi.useCreateNewsMutation, + useUpdateMutation: newsApi.useUpdateNewsMutation, + useDeleteMutation: newsApi.useDeleteNewsMutation, + }, + promo: { + useListQuery: promoApi.useGetPromoListQuery, + useItemQuery: promoApi.useGetPromoItemQuery, + useCreateMutation: promoApi.useCreatePromoMutation, + useUpdateMutation: promoApi.useUpdatePromoMutation, + useDeleteMutation: promoApi.useDeletePromoMutation, + }, + disease: { + useListQuery: diseaseApi.useGetDiseaseListQuery, + useItemQuery: diseaseApi.useGetDiseaseItemQuery, + useCreateMutation: diseaseApi.useCreateDiseaseMutation, + useUpdateMutation: diseaseApi.useUpdateDiseaseMutation, + useDeleteMutation: diseaseApi.useDeleteDiseaseMutation, + }, + 'medical-center': { + useListQuery: medicalCenterApi.useGetMedicalCenterListQuery, + useItemQuery: medicalCenterApi.useGetMedicalCenterItemQuery, + useCreateMutation: medicalCenterApi.useCreateMedicalCenterMutation, + useUpdateMutation: medicalCenterApi.useUpdateMedicalCenterMutation, + useDeleteMutation: medicalCenterApi.useDeleteMedicalCenterMutation, + }, + article: { + useListQuery: articleApi.useGetArticleListQuery, + useItemQuery: articleApi.useGetArticleItemQuery, + useCreateMutation: articleApi.useCreateArticleMutation, + useUpdateMutation: articleApi.useUpdateArticleMutation, + useDeleteMutation: articleApi.useDeleteArticleMutation, + }, + 'site-services': { + useListQuery: siteServicesApi.useGetSiteServicesListQuery, + useItemQuery: siteServicesApi.useGetSiteServicesItemQuery, + useCreateMutation: siteServicesApi.useCreateSiteServicesMutation, + useUpdateMutation: siteServicesApi.useUpdateSiteServicesMutation, + useDeleteMutation: siteServicesApi.useDeleteSiteServicesMutation, + }, +} diff --git a/src/api/apiDisease.js b/src/api/apiDisease.js deleted file mode 100644 index 7f24f5d..0000000 --- a/src/api/apiDisease.js +++ /dev/null @@ -1,55 +0,0 @@ -import { API, authHeader } from './apiSlice'; - -export const diseaseApi = API.injectEndpoints({ - endpoints: (build) => ({ - getDiseaseList: build.query({ - query: ({ search = '', page = '' }) => { - let queryString = '?'; - if (page) queryString += `page=${page}&perPage=20`; - else queryString += `perPage=20`; - if (search) queryString += `&search=${encodeURIComponent(search)}`; - return { - url: `/disease/list${queryString}`, - }; - }, - refetchOnMountOrArgChange: true, - keepUnusedDataFor: 0, - }), - getDisease: build.query({ - query: ({ diseaseId }) => ({ - url: `/disease/${diseaseId}`, - }), - }), - createDisease: build.mutation({ - query: ({ data }) => ({ - url: `/disease/create`, - method: 'POST', - headers: authHeader(), - body: JSON.stringify(data), - }), - }), - updateDisease: build.mutation({ - query: ({ diseaseId, data }) => ({ - url: `/disease/${diseaseId}`, - method: 'PUT', - headers: authHeader(), - body: JSON.stringify(data), - }), - }), - deleteDisease: build.mutation({ - query: ({ diseaseId }) => ({ - url: `/disease/${diseaseId}`, - method: 'DELETE', - headers: authHeader(), - }), - }), - }), -}); - -export const { - useGetDiseaseQuery, - useGetDiseaseListQuery, - useCreateDiseaseMutation, - useUpdateDiseaseMutation, - useDeleteDiseaseMutation, -} = diseaseApi; diff --git a/src/api/apiMedicalCenter.js b/src/api/apiMedicalCenter.js deleted file mode 100644 index 115078f..0000000 --- a/src/api/apiMedicalCenter.js +++ /dev/null @@ -1,55 +0,0 @@ -import { API, authHeader } from './apiSlice'; - -export const medicalCenterApi = API.injectEndpoints({ - endpoints: (build) => ({ - getMedicalCenterList: build.query({ - query: ({ search = '', page = '' }) => { - let queryString = '?'; - if (page) queryString += `page=${page}&perPage=20`; - else queryString += `perPage=20`; - if (search) queryString += `&search=${encodeURIComponent(search)}`; - return { - url: `/medical-center/list${queryString}`, - }; - }, - refetchOnMountOrArgChange: true, - keepUnusedDataFor: 0, - }), - getMedicalCenter: build.query({ - query: ({ medicalCenterId }) => ({ - url: `/medical-center/${medicalCenterId}`, - }), - }), - createMedicalCenter: build.mutation({ - query: ({ data }) => ({ - url: `/medical-center/create`, - method: 'POST', - headers: authHeader(), - body: JSON.stringify(data), - }), - }), - updateMedicalCenter: build.mutation({ - query: ({ medicalCenterId, data }) => ({ - url: `/medical-center/${medicalCenterId}`, - method: 'PUT', - headers: authHeader(), - body: JSON.stringify(data), - }), - }), - deleteMedicalCenter: build.mutation({ - query: ({ medicalCenterId }) => ({ - url: `/medical-center/${medicalCenterId}`, - method: 'DELETE', - headers: authHeader(), - }), - }), - }), -}); - -export const { - useGetMedicalCenterQuery, - useGetMedicalCenterListQuery, - useCreateMedicalCenterMutation, - useUpdateMedicalCenterMutation, - useDeleteMedicalCenterMutation, -} = medicalCenterApi; diff --git a/src/api/apiNews.js b/src/api/apiNews.js deleted file mode 100644 index 6f29b87..0000000 --- a/src/api/apiNews.js +++ /dev/null @@ -1,55 +0,0 @@ -import { API, authHeader } from './apiSlice'; - -export const newsApi = API.injectEndpoints({ - endpoints: (build) => ({ - getNewsList: build.query({ - query: ({ search = '', page = '' }) => { - let queryString = '?'; - if (page) queryString += `page=${page}&perPage=20`; - else queryString += `perPage=20`; - if (search) queryString += `&search=${encodeURIComponent(search)}`; - return { - url: `/news/list${queryString}`, - }; - }, - refetchOnMountOrArgChange: true, - keepUnusedDataFor: 0, - }), - getNews: build.query({ - query: ({ newsId }) => ({ - url: `/news/${newsId}`, - }), - }), - createNews: build.mutation({ - query: ({ data }) => ({ - url: `/news/create`, - method: 'POST', - headers: authHeader(), - body: JSON.stringify(data), - }), - }), - updateNews: build.mutation({ - query: ({ newsId, data }) => ({ - url: `/news/${newsId}`, - method: 'PUT', - headers: authHeader(), - body: JSON.stringify(data), - }), - }), - deleteNews: build.mutation({ - query: ({ newsId }) => ({ - url: `/news/${newsId}`, - method: 'DELETE', - headers: authHeader(), - }), - }), - }), -}); - -export const { - useGetNewsQuery, - useGetNewsListQuery, - useCreateNewsMutation, - useUpdateNewsMutation, - useDeleteNewsMutation, -} = newsApi; diff --git a/src/api/apiSitePromo.js b/src/api/apiSitePromo.js deleted file mode 100644 index 429714f..0000000 --- a/src/api/apiSitePromo.js +++ /dev/null @@ -1,55 +0,0 @@ -import { API, authHeader } from './apiSlice'; - -export const promoApi = API.injectEndpoints({ - endpoints: (build) => ({ - getSitePromoList: build.query({ - query: ({ search = '', page = '' }) => { - let queryString = '?'; - if (page) queryString += `page=${page}&perPage=20`; - else queryString += `perPage=20`; - if (search) queryString += `&search=${encodeURIComponent(search)}`; - return { - url: `/promo/list${queryString}`, - }; - }, - refetchOnMountOrArgChange: true, - keepUnusedDataFor: 0, - }), - getSitePromo: build.query({ - query: ({ promoId }) => ({ - url: `/promo/${promoId}`, - }), - }), - createSitePromo: build.mutation({ - query: ({ data }) => ({ - url: `/promo/create`, - method: 'POST', - headers: authHeader(), - body: JSON.stringify(data), - }), - }), - updateSitePromo: build.mutation({ - query: ({ promoId, data }) => ({ - url: `/promo/${promoId}`, - method: 'PUT', - headers: authHeader(), - body: JSON.stringify(data), - }), - }), - deleteSitePromo: build.mutation({ - query: ({ promoId }) => ({ - url: `/promo/${promoId}`, - method: 'DELETE', - headers: authHeader(), - }), - }), - }), -}); - -export const { - useGetSitePromoQuery, - useGetSitePromoListQuery, - useCreateSitePromoMutation, - useUpdateSitePromoMutation, - useDeleteSitePromoMutation, -} = promoApi; diff --git a/src/api/apiSiteServices.js b/src/api/apiSiteServices.js deleted file mode 100644 index e11d9bc..0000000 --- a/src/api/apiSiteServices.js +++ /dev/null @@ -1,55 +0,0 @@ -import { API, authHeader } from './apiSlice'; - -export const siteServicesApi = API.injectEndpoints({ - endpoints: (build) => ({ - getSiteServicesList: build.query({ - query: ({ search = '', page = '' }) => { - let queryString = '?'; - if (page) queryString += `page=${page}&perPage=20`; - else queryString += `perPage=20`; - if (search) queryString += `&search=${encodeURIComponent(search)}`; - return { - url: `/site-services/list${queryString}`, - }; - }, - refetchOnMountOrArgChange: true, - keepUnusedDataFor: 0, - }), - getSiteServices: build.query({ - query: ({ siteServicesId }) => ({ - url: `/site-services/${siteServicesId}`, - }), - }), - createSiteServices: build.mutation({ - query: ({ data }) => ({ - url: `/site-services/create`, - method: 'POST', - headers: authHeader(), - body: JSON.stringify(data), - }), - }), - updateSiteServices: build.mutation({ - query: ({ siteServicesId, data }) => ({ - url: `/site-services/${siteServicesId}`, - method: 'PUT', - headers: authHeader(), - body: JSON.stringify(data), - }), - }), - deleteSiteServices: build.mutation({ - query: ({ siteServicesId }) => ({ - url: `/site-services/${siteServicesId}`, - method: 'DELETE', - headers: authHeader(), - }), - }), - }), -}); - -export const { - useGetSiteServicesQuery, - useGetSiteServicesListQuery, - useCreateSiteServicesMutation, - useUpdateSiteServicesMutation, - useDeleteSiteServicesMutation, -} = siteServicesApi; diff --git a/src/config/api.js b/src/config/api.js new file mode 100644 index 0000000..53c5439 --- /dev/null +++ b/src/config/api.js @@ -0,0 +1,7 @@ +export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'https://api.sovamed.ru' + +export const apiUrl = (path) => { + const base = API_BASE_URL.replace(/\/$/, '') + const suffix = path.startsWith('/') ? path : `/${path}` + return `${base}${suffix}` +} diff --git a/src/config/contentResources.js b/src/config/contentResources.js new file mode 100644 index 0000000..02795e1 --- /dev/null +++ b/src/config/contentResources.js @@ -0,0 +1,223 @@ +const baseContentFields = [ + { key: 'name', label: 'Название', type: 'text' }, + { key: 'active', label: 'Активно', type: 'checkbox' }, + { key: 'regionId', label: 'Регион', type: 'region' }, + { key: 'alias', label: 'Alias', type: 'text' }, + { key: 'anons', label: 'Анонс', type: 'html' }, + { key: 'content', label: 'Контент', type: 'html' }, +] + +const json = (key, label) => ({ key, label: `${label} (JSON)`, type: 'json' }) +const text = (key, label) => ({ key, label, type: 'text' }) + +export const CONTENT_RESOURCES = { + news: { + slug: 'news', + basePath: '/news', + title: 'Новости', + titleSingle: 'новость', + icon: 'fas fa-newspaper', + listColumns: [ + { key: 'id', label: 'ID' }, + { key: 'name', label: 'Название' }, + { key: 'alias', label: 'Alias' }, + { key: 'active', label: 'Активно', format: 'bool' }, + { key: 'regionId', label: 'Регион' }, + ], + fields: [ + ...baseContentFields, + text('shortName', 'Короткое название'), + text('linkElPrice', 'Ссылка на прайс'), + text('timer', 'Таймер'), + text('timerBg', 'Фон таймера'), + json('formOrder', 'formOrder'), + json('linkServices', 'linkServices'), + json('linkStaff', 'linkStaff'), + json('photos', 'photos'), + ], + }, + promo: { + slug: 'site-promo', + basePath: '/promo', + title: 'Промо (контент)', + titleSingle: 'промо', + icon: 'fas fa-bullhorn', + listColumns: [ + { key: 'id', label: 'ID' }, + { key: 'name', label: 'Название' }, + { key: 'alias', label: 'Alias' }, + { key: 'active', label: 'Активно', format: 'bool' }, + { key: 'regionId', label: 'Регион' }, + ], + fields: [ + ...baseContentFields, + text('shortName', 'Короткое название'), + text('period', 'Период'), + text('timer', 'Таймер'), + text('timerBg', 'Фон таймера'), + json('clinics', 'clinics'), + json('linkServices', 'linkServices'), + json('linkStaff', 'linkStaff'), + json('photos', 'photos'), + ], + }, + disease: { + slug: 'disease', + basePath: '/disease', + title: 'Заболевания', + titleSingle: 'заболевание', + icon: 'fas fa-heartbeat', + listColumns: [ + { key: 'id', label: 'ID' }, + { key: 'name', label: 'Название' }, + { key: 'alias', label: 'Alias' }, + { key: 'active', label: 'Активно', format: 'bool' }, + ], + fields: [ + ...baseContentFields, + text('previewPicture', 'previewPicture'), + { key: 'hidePicture', label: 'hidePicture', type: 'checkbox' }, + text('readTime', 'readTime'), + text('diseasesName', 'diseasesName'), + text('diseasesOtherName', 'diseasesOtherName'), + text('symptom', 'symptom'), + text('staff', 'staff'), + text('bibliography', 'bibliography'), + json('tagsImportant', 'tagsImportant'), + json('tags', 'tags'), + json('linkServices', 'linkServices'), + json('staffList', 'staffList'), + json('staffPost', 'staffPost'), + json('staffPostExclude', 'staffPostExclude'), + json('linkFaq', 'linkFaq'), + json('staffCheck', 'staffCheck'), + ], + }, + 'medical-center': { + slug: 'medical-center', + basePath: '/medical-center', + title: 'Медцентры', + titleSingle: 'медцентр', + icon: 'fas fa-hospital', + listColumns: [ + { key: 'id', label: 'ID' }, + { key: 'name', label: 'Название' }, + { key: 'alias', label: 'Alias' }, + { key: 'active', label: 'Активно', format: 'bool' }, + ], + fields: [ + ...baseContentFields, + text('mainLinkStaff', 'mainLinkStaff'), + text('plusText', 'plusText'), + text('plusTitle', 'plusTitle'), + text('processText', 'processText'), + text('processTitle', 'processTitle'), + text('servicesTitle', 'servicesTitle'), + text('trainingText', 'trainingText'), + text('trainingTextTitle', 'trainingTextTitle'), + text('whyText', 'whyText'), + text('whyTitle', 'whyTitle'), + { key: 'hidePicture', label: 'hidePicture', type: 'number' }, + json('kodUslug', 'kodUslug'), + json('doctors', 'doctors'), + json('services', 'services'), + json('articles', 'articles'), + json('txtUp', 'txtUp'), + json('contraindications', 'contraindications'), + json('indications', 'indications'), + json('linkSale', 'linkSale'), + json('plusList', 'plusList'), + json('servicesList', 'servicesList'), + json('servicesPhotos', 'servicesPhotos'), + json('sortStaff', 'sortStaff'), + ], + }, + article: { + slug: 'article', + basePath: '/article', + title: 'Статьи', + titleSingle: 'статью', + icon: 'fas fa-file-alt', + listUsesMeta: true, + listUsesLimit: true, + listColumns: [ + { key: 'id', label: 'ID' }, + { key: 'name', label: 'Название' }, + { key: 'alias', label: 'Alias' }, + { key: 'active', label: 'Активно', format: 'bool' }, + ], + fields: [ + ...baseContentFields, + text('previewPicture', 'previewPicture'), + json('doctors', 'doctors'), + json('services', 'services'), + ], + }, + 'site-services': { + slug: 'site-services', + basePath: '/site-services', + title: 'Услуги сайта', + titleSingle: 'услугу', + icon: 'fas fa-concierge-bell', + listColumns: [ + { key: 'id', label: 'ID' }, + { key: 'name', label: 'Название' }, + { key: 'alias', label: 'Alias' }, + { key: 'active', label: 'Активно', format: 'bool' }, + ], + fields: [ + ...baseContentFields, + text('previewImg', 'previewImg'), + text('partPrice', 'partPrice'), + text('pokazaniya', 'pokazaniya'), + text('preparation', 'preparation'), + text('protivopokazaniya', 'protivopokazaniya'), + text('bannerImg', 'bannerImg'), + text('bannerImgM', 'bannerImgM'), + text('bannerImgUrl', 'bannerImgUrl'), + text('downloadFile', 'downloadFile'), + text('fullWidthBanner', 'fullWidthBanner'), + text('kodUslug', 'kodUslug'), + text('linkPrice', 'linkPrice'), + text('photosTitle', 'photosTitle'), + text('contraindicationsList', 'contraindicationsList'), + text('customBlockText', 'customBlockText'), + text('customBlockText2', 'customBlockText2'), + text('customBlockTitle', 'customBlockTitle'), + text('customBlockTitle2', 'customBlockTitle2'), + text('indicationsList', 'indicationsList'), + text('plusList', 'plusList'), + text('plusText', 'plusText'), + text('plusTitle', 'plusTitle'), + text('prepareTitle', 'prepareTitle'), + text('processText', 'processText'), + text('processTitle', 'processTitle'), + text('servicesList', 'servicesList'), + text('servicesTitle', 'servicesTitle'), + text('textUp', 'textUp'), + text('trainingText', 'trainingText'), + text('whyText', 'whyText'), + text('whyTitle', 'whyTitle'), + { key: 'hidePicture', label: 'hidePicture', type: 'number' }, + json('linkVideoreviews', 'linkVideoreviews'), + json('faq', 'faq'), + json('hideSignBtn', 'hideSignBtn'), + json('quiz', 'quiz'), + json('tags', 'tags'), + json('tagsImportant', 'tagsImportant'), + json('clinics', 'clinics'), + json('staffUp', 'staffUp'), + json('advantages', 'advantages'), + json('saleId', 'saleId'), + json('sortStaff', 'sortStaff'), + json('linkArticlesServices', 'linkArticlesServices'), + json('servicesPhotos', 'servicesPhotos'), + json('linkFaq', 'linkFaq'), + json('linkServices', 'linkServices'), + json('linkStaff', 'linkStaff'), + json('photos', 'photos'), + ], + }, +} + +export const CONTENT_RESOURCE_KEYS = Object.keys(CONTENT_RESOURCES) diff --git a/src/pages/AddArticlePage.jsx b/src/pages/AddArticlePage.jsx deleted file mode 100644 index 7f4ef38..0000000 --- a/src/pages/AddArticlePage.jsx +++ /dev/null @@ -1,88 +0,0 @@ -import { useState, useEffect } from 'react'; -import { useParams, useNavigate } from 'react-router-dom'; -import { useSelector } from 'react-redux'; -import { useCreateArticleMutation } from '/src/api/apiArticle'; -import { selectRegions } from '../store/slice/regionSlice'; -import { TextEditor } from '../components/Editors/TextEditor'; -import { LoadingComponent } from '../components/Placeholders/LoadingComponent'; -import { ErrorComponent } from '../components/Placeholders/ErrorComponent'; -import { NotFindElement } from '../components/Placeholders/NotFindElement'; -import { EditElementForm } from '../components/Forms/EditElementForm'; -import Modal from '../components/Modals/Modal'; - -export function AddArticlePage() { - - const navigate = useNavigate(); - const navigateBack = () => navigate(`/article`); - const regions = useSelector(selectRegions); - const [createArticle] = useCreateArticleMutation(); - const [isModalSuccess, setModalSuccess] = useState(false); - const [errors, setErrors] = useState({ name: '', alias: '', regionId: '' }); - const [form, setForm] = useState({ - name: '', - active: false, - regionId: '', - alias: '', - previewPicture: '', - doctors: '', - services: '', - }); - const [anons, setAnons] = useState(''); - const [content, setContent] = useState(''); - - - - const handleChange = (key) => (e) => { - const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value; - setForm((f) => ({ ...f, [key]: value })); - if (key === 'name' || key === 'alias' || key === 'regionId') setErrors((err) => ({ ...err, [key]: '' })); - }; - - - - const handleSave = async () => { - const newErrors = { name: '', alias: '', regionId: '' }; - let hasError = false; - if (!String(form.name ?? '').trim()) { newErrors.name = 'Название не может быть пустым'; hasError = true; } - if (!String(form.alias ?? '').trim()) { newErrors.alias = 'Alias не может быть пустым'; hasError = true; } - if (form.regionId === '' || form.regionId == null) { newErrors.regionId = 'Укажите регион'; hasError = true; } - if (hasError) { setErrors(newErrors); window.alert('Пожалуйста исправьте ошибки в форме.'); return; } - const data = { anons, content }; - try { - data.name = form.name === '' ? null : form.name; - data.active = Boolean(form.active); - data.regionId = form.regionId === '' ? null : Number(form.regionId); - data.alias = form.alias === '' ? null : form.alias; - data.previewPicture = form.previewPicture === '' ? null : form.previewPicture; - data.doctors = !form.doctors || !String(form.doctors).trim() ? null : JSON.parse(form.doctors); - data.services = !form.services || !String(form.services).trim() ? null : JSON.parse(form.services); - } catch (e) { window.alert('Пожалуйста исправьте ошибки в форме.'); return; } - try { - const response = await createArticle({ data }).unwrap(); - setModalSuccess(true); - window.setTimeout(() => { navigate(`/article/edit/${response.id}`); window.location.reload(); }, 2000); - } catch (err) { - console.error('Ошибка сохранения:', err); - } - }; - - - - return ( - {}} isAddSpecialist={true}> -
- {errors.name && {errors.name}}
-
-
- {errors.regionId && {errors.regionId}}
-
- {errors.alias && {errors.alias}}
-
-
-
-