fix(question): show both language fields for MCQ options

Options editor used a single toggle-based bilingual field (BilingualInput)
that swapped English/Amharic in place via a tiny, easy-to-miss button.
Authors were filling English only and never noticing Amharic was empty.

- QuestionOptionsEditor: show separate always-visible English/Amharic
  TextInputs per option instead of the toggle field.
- BilingualInput (shared): for other callers still using the toggle,
  add a tooltip and a red dot indicator when the hidden language is
  empty, so the gap is visible without switching.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
mihretue
2026-08-17 08:26:21 +00:00
parent 4edce751ee
commit 84ff644ebb
4 changed files with 81 additions and 42 deletions

View File

@@ -1,8 +1,8 @@
import { useEffect, useState } from 'react';
import { ActionIcon, Alert, Button, Checkbox, Group, Loader, Stack, Text } from '@mantine/core';
import { ActionIcon, Alert, Button, Checkbox, Group, Loader, Stack, Text, TextInput } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { IconGripVertical, IconInfoCircle, IconPlus, IconTrash } from '@tabler/icons-react';
import { BilingualInput, notify, useErrorHandler } from '@ema-platform/ui';
import { notify, useErrorHandler } from '@ema-platform/ui';
import type { BilingualValue } from '@ema-platform/ui';
import {
useGetQuestionWithOptionsQuery,
@@ -48,8 +48,10 @@ export function QuestionOptionsEditor({ questionId }: { questionId: string }) {
// pretending to know what they were.
}, [question]);
const updateText = (index: number, text: BilingualValue) => {
setDraft((prev) => prev.map((o, i) => (i === index ? { ...o, text } : o)));
const updateField = (index: number, lang: keyof BilingualValue, value: string) => {
setDraft((prev) =>
prev.map((o, i) => (i === index ? { ...o, text: { ...o.text, [lang]: value } } : o)),
);
};
const toggleCorrect = (index: number) => {
@@ -93,26 +95,33 @@ export function QuestionOptionsEditor({ questionId }: { questionId: string }) {
{t('question.options.hint')}
</Alert>
{draft.map((option, index) => (
<Group key={index} gap="xs" wrap="nowrap" align="flex-end">
<IconGripVertical size={16} style={{ opacity: 0.4, marginBottom: 8 }} />
<BilingualInput
label={t('question.options.optionLabel', { number: index + 1 })}
value={option.text}
onChange={(v) => updateText(index, v)}
<Group key={index} gap="xs" wrap="nowrap" align="center">
<IconGripVertical size={16} style={{ opacity: 0.4 }} />
<Stack gap={6} style={{ flex: 1 }}>
<TextInput
label={t('question.options.optionEn', { number: index + 1 })}
value={option.text.en}
onChange={(e) => updateField(index, 'en', e.currentTarget.value)}
size="sm"
style={{ flex: 1 }}
required
/>
<TextInput
label={t('question.options.optionAm', { number: index + 1 })}
value={option.text.am}
onChange={(e) => updateField(index, 'am', e.currentTarget.value)}
size="sm"
required
/>
</Stack>
<Checkbox
label={t('question.options.correct')}
checked={option.isCorrect}
onChange={() => toggleCorrect(index)}
mb={4}
/>
<ActionIcon
variant="subtle"
color="red"
size="sm"
mb={4}
disabled={draft.length <= 2}
onClick={() => removeOption(index)}
>

View File

@@ -742,6 +742,8 @@ export const am: Translations = {
title: "የመልስ አማራጮች",
hint: "ትክክለኛውን አማራጭ ምረጥ/ምረጪ። ማስቀመጥ መላውን የአማራጭ ስብስብ ይተካል።",
optionLabel: "አማራጭ {{number}}",
optionEn: "አማራጭ {{number}} (እንግሊዝኛ)",
optionAm: "አማራጭ {{number}} (አማርኛ)",
correct: "ትክክለኛ",
addOption: "አማራጭ ጨምር",
save: "አማራጮችን አስቀምጥ",

View File

@@ -743,6 +743,8 @@ export const en = {
title: 'Answer Options',
hint: 'Mark every correct option. Saving replaces the entire option set.',
optionLabel: 'Option {{number}}',
optionEn: 'Option {{number}} (English)',
optionAm: 'Option {{number}} (Amharic)',
correct: 'Correct',
addOption: 'Add option',
save: 'Save options',

View File

@@ -1,5 +1,6 @@
import { useState } from 'react';
import {
Tooltip,
TextInput,
UnstyledButton,
rem,
@@ -37,6 +38,9 @@ export function BilingualInput({
...rest
}: BilingualInputProps) {
const [lang, setLang] = useState<'en' | 'am'>('en');
const otherLang = lang === 'en' ? 'am' : 'en';
const otherLangName = otherLang === 'en' ? 'English' : 'Amharic';
const otherIsEmpty = !value[otherLang]?.trim();
const toggle = () => setLang((l) => (l === 'en' ? 'am' : 'en'));
@@ -48,17 +52,22 @@ export function BilingualInput({
value={value[lang]}
onChange={(e) => onChange({ ...value, [lang]: e.currentTarget.value })}
rightSection={
<Tooltip
label={`Switch to ${otherLangName}${otherIsEmpty ? ' — empty' : ''}`}
withArrow
>
<UnstyledButton
onClick={toggle}
aria-label={`Switch to ${lang === 'en' ? 'Amharic' : 'English'}`}
aria-label={`Switch to ${otherLangName}`}
style={{
position: 'relative',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: rem(28),
height: rem(20),
width: rem(32),
height: rem(24),
borderRadius: rem(4),
fontSize: rem(10),
fontSize: rem(11),
fontWeight: 700,
letterSpacing: '0.05em',
background:
@@ -74,11 +83,28 @@ export function BilingualInput({
}}
>
{lang === 'en' ? 'EN' : 'AM'}
{otherIsEmpty && (
<span
aria-hidden
title={`${otherLangName} text is missing`}
style={{
position: 'absolute',
top: -2,
right: -2,
width: rem(7),
height: rem(7),
borderRadius: '50%',
background: 'var(--mantine-color-red-6)',
border: '1px solid var(--mantine-color-body)',
}}
/>
)}
</UnstyledButton>
</Tooltip>
}
styles={{
input: {
paddingRight: rem(42),
paddingRight: rem(46),
},
}}
{...rest}