diff --git a/src/components/Field/Field.module.scss b/src/components/Field/Field.module.scss index 2bed1ff..c19a82b 100644 --- a/src/components/Field/Field.module.scss +++ b/src/components/Field/Field.module.scss @@ -12,6 +12,9 @@ color: $gray-500; font-family: $font-sans; font-size: $fs-caption; + // Подсказка может содержать несколько сносок через \n (см. anonymousHint) — + // pre-line превращает переносы строк в тексте в реальные строки на экране. + white-space: pre-line; } // Красная рамка у контрола внутри — ui-kit не знает о состоянии ошибки, diff --git a/src/features/feedback/FeedbackForm.tsx b/src/features/feedback/FeedbackForm.tsx index 20868a8..e69cd8e 100644 --- a/src/features/feedback/FeedbackForm.tsx +++ b/src/features/feedback/FeedbackForm.tsx @@ -24,7 +24,10 @@ export const FeedbackForm: React.FC = () => { } return ( - + // FormCard требует title, но заголовок страницы уже показан в PageShell + // (App.tsx) — второй здесь был бы дублем. Пустая строка убирает текст, + // а гэпа не оставляет: у ui-kit пустой title-блок не создаёт строки. + {formError ? (
{formError} diff --git a/src/features/feedback/model.ts b/src/features/feedback/model.ts index 87e7933..43cf2ad 100644 --- a/src/features/feedback/model.ts +++ b/src/features/feedback/model.ts @@ -45,27 +45,42 @@ export const KIND_OPTIONS: ISelectOption[] = [ { value: 'other', label: 'Другое' } ]; -/** Какие текстовые поля показываем для выбранного вида обращения. */ -export interface IVisibleAppealFields { - situation: boolean; - suggestion: boolean; +/** Показывать поле или нет, и если показывать — обязательно ли оно. */ +export interface IAppealFieldConfig { + visible: boolean; + required: boolean; +} + +/** Настройка текстовых полей для выбранного вида обращения. */ +export interface IAppealFieldsConfig { + situation: IAppealFieldConfig; + suggestion: IAppealFieldConfig; } /** * Состав полей зависит от вида обращения: - * жалоба — что случилось и что предлагаете изменить; + * жалоба — обязательно описание, а предложение — на желание автора; * предложение — только само предложение, «ситуации» ещё нет; * другое — свободное описание, без предложения. * Одно правило на форму и на валидацию, чтобы они не разошлись. */ -export function getVisibleAppealFields(kind: AppealKind): IVisibleAppealFields { +export function getAppealFieldsConfig(kind: AppealKind): IAppealFieldsConfig { switch (kind) { case 'complaint': - return { situation: true, suggestion: true }; + return { + situation: { visible: true, required: true }, + suggestion: { visible: true, required: false } + }; case 'suggestion': - return { situation: false, suggestion: true }; + return { + situation: { visible: false, required: false }, + suggestion: { visible: true, required: true } + }; case 'other': - return { situation: true, suggestion: false }; + return { + situation: { visible: true, required: true }, + suggestion: { visible: false, required: false } + }; } } diff --git a/src/features/feedback/sections/AppealSection.tsx b/src/features/feedback/sections/AppealSection.tsx index 69914e9..2997bda 100644 --- a/src/features/feedback/sections/AppealSection.tsx +++ b/src/features/feedback/sections/AppealSection.tsx @@ -1,6 +1,6 @@ import { FormGrid, Select, TextArea, TextInput } from '@tnsu/ui-kit'; import { Field } from '../../../components/Field/Field'; -import { getVisibleAppealFields, KIND_OPTIONS, type AppealKind } from '../model'; +import { getAppealFieldsConfig, KIND_OPTIONS, type AppealKind } from '../model'; import { TEXTS } from '../texts'; import type { ISectionProps } from './ISectionProps'; @@ -9,7 +9,7 @@ const SUGGESTION_ROWS = 4; /** Суть обращения: вид и зависящие от него текстовые поля. */ export const AppealSection: React.FC = ({ data, errors, set }) => { - const visible = getVisibleAppealFields(data.kind); + const fields = getAppealFieldsConfig(data.kind); const isOther = data.kind === 'other'; return ( @@ -39,11 +39,11 @@ export const AppealSection: React.FC = ({ data, errors, set }) => ) : null} - {visible.situation ? ( + {fields.situation.visible ? ( @@ -59,11 +59,11 @@ export const AppealSection: React.FC = ({ data, errors, set }) => ) : null} - {visible.suggestion ? ( + {fields.suggestion.visible ? ( diff --git a/src/features/feedback/texts.ts b/src/features/feedback/texts.ts index 7f5aeea..b3b8f5a 100644 --- a/src/features/feedback/texts.ts +++ b/src/features/feedback/texts.ts @@ -6,10 +6,11 @@ export const TEXTS = { pageTitle: 'Обращение', pageSubtitle: 'Данное обращение будет направлено в HR департамент', - formTitle: 'Обращение в HR департамент', - + // Две сноски с разным числом звёздочек — как в исходной форме, чтобы читалось + // как два отдельных примечания, а не слитный абзац. Field рендерит их построчно. anonymousHint: - 'Анонимное обращение: мы не сможем дать вам обратную связь. Именные обращения обрабатываются в течение 3–5 рабочих дней, если вопрос не требует большего срока.', + '* Если обращение анонимное, мы не сможем дать вам обратную связь.\n' + + '** Именные обращения обрабатываются в течение 3–5 рабочих дней, если вопрос не требует большего срока.', labels: { company: 'Компания', diff --git a/src/features/feedback/validateFeedback.ts b/src/features/feedback/validateFeedback.ts index eba6b3e..9bba66a 100644 --- a/src/features/feedback/validateFeedback.ts +++ b/src/features/feedback/validateFeedback.ts @@ -1,4 +1,4 @@ -import { getVisibleAppealFields, type FeedbackErrors, type IFeedbackData } from './model'; +import { getAppealFieldsConfig, type FeedbackErrors, type IFeedbackData } from './model'; import { TEXTS } from './texts'; /** Ниже этой длины текст не считаем содержательным описанием. */ @@ -26,22 +26,24 @@ export function validateFeedback(data: IFeedbackData): FeedbackErrors { errors.kindOther = TEXTS.errors.required; } - // Спрашиваем только то, что реально показано для выбранного вида обращения: - // иначе форма ругалась бы на скрытое поле, и пользователь не понял бы, где ошибка. - const visible = getVisibleAppealFields(data.kind); + // Правило видимости/обязательности общее с формой (AppealSection), поэтому + // валидируем по нему же — иначе форма ругалась бы на скрытое или необязательное поле. + const fields = getAppealFieldsConfig(data.kind); - if (visible.situation) { - if (!data.situation.trim()) { + if (fields.situation.visible) { + const value = data.situation.trim(); + if (fields.situation.required && !value) { errors.situation = TEXTS.errors.required; - } else if (data.situation.trim().length < MIN_TEXT_LENGTH) { + } else if (value && value.length < MIN_TEXT_LENGTH) { errors.situation = TEXTS.errors.tooShort; } } - if (visible.suggestion) { - if (!data.suggestion.trim()) { + if (fields.suggestion.visible) { + const value = data.suggestion.trim(); + if (fields.suggestion.required && !value) { errors.suggestion = TEXTS.errors.required; - } else if (data.suggestion.trim().length < MIN_TEXT_LENGTH) { + } else if (value && value.length < MIN_TEXT_LENGTH) { errors.suggestion = TEXTS.errors.tooShort; } }