diff --git a/src/components/content-management/DynamicMatchingSlotField.tsx b/src/components/content-management/DynamicMatchingSlotField.tsx new file mode 100644 index 0000000..d342c7e --- /dev/null +++ b/src/components/content-management/DynamicMatchingSlotField.tsx @@ -0,0 +1,241 @@ +import { Plus, Trash2 } from "lucide-react" +import { Button } from "../ui/button" +import { Input } from "../ui/input" +import { + addMatchingInputRow, + addMatchingPair, + defaultMatchingAnswerFromInputs, + MATCHING_MIN_ITEMS, + parseMatchingAnswerSlotValue, + parseMatchingInputsSlotValue, + removeMatchingInputRow, + removeMatchingPair, + serializeMatchingAnswerSlotValue, + serializeMatchingInputsSlotValue, + type MatchingAnswerSlotValue, + type MatchingInputsSlotValue, +} from "../../lib/matchingSlotValue" + +export function DynamicMatchingInputsSlot({ + value, + onChange, + disabled, + slotLabel, +}: { + value: string + onChange: (next: string) => void + disabled: boolean + slotLabel: string +}) { + const parsed = parseMatchingInputsSlotValue(value) + + const updateValue = (next: MatchingInputsSlotValue) => { + onChange(serializeMatchingInputsSlotValue(next)) + } + + const updateSide = ( + side: "left" | "right", + index: number, + text: string, + ) => { + const next = { + left: [...parsed.left], + right: [...parsed.right], + } + next[side][index] = { ...next[side][index], text } + updateValue(next) + } + + const rowCount = Math.max(parsed.left.length, parsed.right.length) + const canRemove = rowCount > MATCHING_MIN_ITEMS + + return ( +
+ Left {parsed.left[index]?.id ?? `l${index + 1}`} +
+ updateSide("left", index, e.target.value)} + placeholder={`Left item ${index + 1}`} + className="rounded-lg border-grayScale-200 bg-white" + disabled={disabled} + /> ++ Right {parsed.right[index]?.id ?? `r${index + 1}`} +
+ updateSide("right", index, e.target.value)} + placeholder={`Right item ${index + 1}`} + className="rounded-lg border-grayScale-200 bg-white" + disabled={disabled} + /> ++ Minimum {MATCHING_MIN_ITEMS} rows on each side. Whitespace in text is preserved. +
++ Fill in matching inputs first so answer pairs can reference left and right ids. +
+ ) : null} + ++ Minimum {MULTIPLE_CHOICE_MIN_OPTIONS} options. Mark one as correct. +
++ Passage segments +
+ {parsed.segments.map((segment, index) => ( ++ Text +
++ Word bank +
+ ++ Minimum {SELECT_MISSING_WORDS_MIN_BANK} words in the bank and at least one blank. + Whitespace in text is preserved. +
++ Fill in the cloze passage and word bank first so answers can reference blank and word ids. +
+ ) : null} + +