diff --git a/index.html b/index.html index 158c5cf..22ff2f9 100644 --- a/index.html +++ b/index.html @@ -5,6 +5,7 @@ Admin panel +
diff --git a/public/env.js b/public/env.js new file mode 100644 index 0000000..c7a5eeb --- /dev/null +++ b/public/env.js @@ -0,0 +1,3 @@ +window.__ENV__ = window.__ENV__ || { + API_BASE_URL: 'http://localhost:8081', +}; 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/apiSlice.js b/src/api/apiSlice.js index 8056e6d..1058946 100644 --- a/src/api/apiSlice.js +++ b/src/api/apiSlice.js @@ -1,4 +1,5 @@ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' +import { API_BASE_URL } from '@/config/api' export const authHeader = () => { const token = localStorage.getItem('token') @@ -8,7 +9,7 @@ export const authHeader = () => { export const API = createApi({ reducerPath: 'API', baseQuery: fetchBaseQuery({ - baseUrl: 'https://api.sovamed.ru', + baseUrl: API_BASE_URL, credentials: 'include', }), endpoints: (builder) => ({ diff --git a/src/components/Docs/Certificates.jsx b/src/components/Docs/Certificates.jsx index e97ebf9..bfcbfa6 100644 --- a/src/components/Docs/Certificates.jsx +++ b/src/components/Docs/Certificates.jsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from 'react'; +import { apiUrl } from '@/config/api'; import { CertEditor } from '../Editors/CertEditor' const isValidImage = (file) => { @@ -15,7 +16,7 @@ export function CertificatesForm({ initCertificates, onChange }) { const [certificates, setCertificates] = useState([]); useEffect(() => { - const certificatesWithPictureUrl = initCertificates.map((init) => ({ ...init, picture: `https://api.sovamed.ru/uploads/${init.picture}`})) + const certificatesWithPictureUrl = initCertificates.map((init) => ({ ...init, picture: apiUrl(`/uploads/${init.picture}`)})) setInitialCertificates([...certificatesWithPictureUrl]) setCertificates([...certificatesWithPictureUrl]) }, [initCertificates]) diff --git a/src/components/Docs/Portfolio.jsx b/src/components/Docs/Portfolio.jsx index 72084bf..ff8897f 100644 --- a/src/components/Docs/Portfolio.jsx +++ b/src/components/Docs/Portfolio.jsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from 'react'; +import { apiUrl } from '@/config/api'; import { CertEditor } from '../Editors/CertEditor' const isValidImage = (file) => { @@ -17,7 +18,7 @@ export function PortfolioForm({ initPortfolios, onChange }) { useEffect(() => { // console.log(initPortfolios) const portfolioWithPictureUrl = initPortfolios.map((init) => { - if ( init.picture ) return ({ ...init, picture: `https://api.sovamed.ru/uploads/${init.picture}`}) + if ( init.picture ) return ({ ...init, picture: apiUrl(`/uploads/${init.picture}`)}) return { ...init } }) setInitialPorfolios([...portfolioWithPictureUrl]) diff --git a/src/components/Docs/Stocks.jsx b/src/components/Docs/Stocks.jsx index 18c6408..edd1471 100644 --- a/src/components/Docs/Stocks.jsx +++ b/src/components/Docs/Stocks.jsx @@ -1,5 +1,6 @@ import React, { useState, useEffect, useRef } from 'react'; +import { apiUrl } from '@/config/api'; import { CertEditor } from '../Editors/CertEditor' const isValidImage = (file) => { @@ -22,7 +23,7 @@ export function StocksForm({ initStocks, onChange }) { // console.log(initStocks[0]?.startDate) // console.log(typeof initStocks[0]?.startDate) const portfolioWithPictureUrl = initStocks.map((init) => { - if ( init.picture ) return ({ ...init, picture: `https://api.sovamed.ru/uploads/${init.picture}`}) + if ( init.picture ) return ({ ...init, picture: apiUrl(`/uploads/${init.picture}`)}) return { ...init } }).map(init => { const dateStart = new Date(init.startDate); diff --git a/src/config/api.js b/src/config/api.js new file mode 100644 index 0000000..6a6ff0d --- /dev/null +++ b/src/config/api.js @@ -0,0 +1,10 @@ +export const API_BASE_URL = + (typeof window !== 'undefined' && window.__ENV__?.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/hooks/useLostLocations.jsx b/src/hooks/useLostLocations.jsx index 58dac55..c054a21 100644 --- a/src/hooks/useLostLocations.jsx +++ b/src/hooks/useLostLocations.jsx @@ -2,6 +2,8 @@ import { useState, useEffect } from 'react'; import axios from 'axios'; import { useSelector } from 'react-redux'; +import { apiUrl } from '@/config/api'; + import { useGetFilialsQuery } from '../api/apiFilial'; import { useGetDepartmentsQuery } from '../api/apiDepartment'; import { selectRegions } from '../store/slice/regionSlice'; @@ -31,7 +33,7 @@ export function useLostDoctors() { Promise.all( data.data.map(item => { const fetchString = fetchParams.filter((param => Boolean(item[param]))).map(param => `${param}=${item[param]}`).join('&'); - return axios.get(`https://api.sovamed.ru/idoctor/list?${fetchString}`, { + return axios.get(apiUrl(`/idoctor/list?${fetchString}`), { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } }) .then(res => { diff --git a/src/hooks/useSpecialist.jsx b/src/hooks/useSpecialist.jsx index 3ce82e2..78f7d38 100644 --- a/src/hooks/useSpecialist.jsx +++ b/src/hooks/useSpecialist.jsx @@ -2,6 +2,8 @@ import { useState, useEffect } from 'react'; import axios from 'axios'; import { useSelector } from 'react-redux'; +import { apiUrl } from '@/config/api'; + import { useGetSpecialistsQuery, useGetSpecialistQuery } from '../api/apiSpecialist'; import { useGetKodopersQuery } from '../api/apiKodoper'; import { useGetFilialsQuery } from '../api/apiFilial'; @@ -27,7 +29,7 @@ export function useSpecialist(id) { Promise.all( specialist.kodoper.map(code => - axios.get(`https://api.sovamed.ru/pricelist/list?search=${code}`, { + axios.get(apiUrl(`/pricelist/list?search=${code}`), { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } }) .then(res => { diff --git a/src/pages/EditFilialPage.jsx b/src/pages/EditFilialPage.jsx index b054812..5fc135b 100644 --- a/src/pages/EditFilialPage.jsx +++ b/src/pages/EditFilialPage.jsx @@ -2,6 +2,8 @@ import { useEffect, useState, useRef } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { useSelector } from 'react-redux'; +import { apiUrl } from '@/config/api'; + import { useGetFilialsQuery, useUpdateFilialMutation, useUploadFilialPictureMutation } from '../api/apiFilial'; import { selectUtils } from '../store/slice/utilsSlice'; import { selectRegions } from '../store/slice/regionSlice'; @@ -69,7 +71,7 @@ export const EditFilialPage = () => { email: filial.email, fid: filial.fid, origin: filial.origin, - picture: `https://api.sovamed.ru${filial.pictureLink}`, + picture: apiUrl(filial.pictureLink), policy: filial.policy, } setForm({... filialData}) diff --git a/src/pages/EditSpecialistPage.jsx b/src/pages/EditSpecialistPage.jsx index 54ddf53..651d939 100644 --- a/src/pages/EditSpecialistPage.jsx +++ b/src/pages/EditSpecialistPage.jsx @@ -2,6 +2,8 @@ import {useEffect, useState, useMemo, useRef} from 'react' import {useParams, useNavigate, useLocation } from 'react-router-dom' import DatePicker from "react-datepicker"; +import { apiUrl } from '@/config/api'; + import { useUpdateSpecialistMutation, useDeleteSpecialistMutation, useUploadSpecialistPictureMutation } from '../api/apiSpecialist' import { useCreateLocationMutation, useUpdateLocationMutation, useDeleteLocationMutation } from '/src/api/apiLocation.js' import { @@ -386,7 +388,7 @@ export const EditSpecialistPage = () => { } updateField('category', formatCategory(specialist.category)); - updateField('previewPicture', `https://api.sovamed.ru${specialist.pictureLink}`); + updateField('previewPicture', apiUrl(specialist.pictureLink)); const formattedDate = specialist.experience ? `${specialist.experience}-01-01` diff --git a/src/pages/EditStockPage.jsx b/src/pages/EditStockPage.jsx index e779c19..89443b4 100644 --- a/src/pages/EditStockPage.jsx +++ b/src/pages/EditStockPage.jsx @@ -1,6 +1,8 @@ import { useState, useEffect, useRef } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; +import { apiUrl } from '@/config/api'; + import { useGetStockQuery, useUpdateStockMutation, @@ -85,7 +87,7 @@ export function EditStockPage() { name: stock.name ?? '', startDate: datetimeLocalStart, endDate: datetimeLocalEnd, - picture: stock.picture ? `https://api.sovamed.ru/uploads/${stock.picture}` : '', + picture: stock.picture ? apiUrl(`/uploads/${stock.picture}`) : '', })); setAnons( stock.anons ) diff --git a/src/pages/content/ContentEditPage.jsx b/src/pages/content/ContentEditPage.jsx new file mode 100644 index 0000000..76e3888 --- /dev/null +++ b/src/pages/content/ContentEditPage.jsx @@ -0,0 +1,370 @@ +import { useEffect, useRef, useState } from 'react' +import { useNavigate, useParams } from 'react-router-dom' +import { useSelector } from 'react-redux' + +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' +import { parseSaveError } from '../../utils/parseSaveError' + +const SUCCESS_MODAL_MS = 2000 + +const emptyFormFromConfig = (fields) => { + const form = {} + fields.forEach((field) => { + if (field.type === 'checkbox') { + form[field.key] = false + } else if (field.type === 'number' || field.type === 'region') { + form[field.key] = '' + } else { + form[field.key] = '' + } + }) + return form +} + +const itemToForm = (item, fields) => { + const form = {} + fields.forEach((field) => { + const value = item[field.key] + if (field.type === 'json') { + form[field.key] = value == null ? '' : JSON.stringify(value, null, 2) + } else if (field.type === 'checkbox') { + form[field.key] = Boolean(value) + } else if (field.type === 'region' || field.type === 'number') { + form[field.key] = value ?? '' + } else { + form[field.key] = value ?? '' + } + }) + return form +} + +const formToPayload = (form, fields) => { + const data = {} + fields.forEach((field) => { + const raw = form[field.key] + if (field.type === 'json') { + if (!raw || !String(raw).trim()) { + data[field.key] = null + return + } + try { + data[field.key] = JSON.parse(raw) + } catch { + const error = new Error('Невалидный JSON — проверьте синтаксис') + error.fieldKey = field.key + throw error + } + return + } + if (field.type === 'checkbox') { + data[field.key] = Boolean(raw) + return + } + if (field.type === 'region' || field.type === 'number') { + data[field.key] = raw === '' ? null : Number(raw) + return + } + data[field.key] = raw === '' ? null : raw + }) + return data +} + +const FieldHint = ({ message }) => + message ? {message} : null + +const fieldWrapperClass = (hasError, extra = '') => + ['form-group', extra, hasError ? 'content-field--has-error' : ''].filter(Boolean).join(' ') + +const ContentField = ({ field, form, updateField, regions, fieldErrors }) => { + const value = form[field.key] + const errorMessage = fieldErrors[field.key] + const hasError = Boolean(errorMessage) + const labelClass = hasError ? 'content-field-error-label' : undefined + const controlClass = `form-control${hasError ? ' is-invalid' : ''}` + + if (field.type === 'checkbox') { + return ( +
+ updateField(field.key, e.target.checked)} + /> + + +
+ ) + } + + if (field.type === 'region') { + const selectValue = value === '' || value == null ? '' : String(value) + return ( +
+ + + +
+ ) + } + + if (field.type === 'html') { + return ( +
+ + updateField(field.key, html)} /> + +
+ ) + } + + if (field.type === 'json') { + return ( +
+ +