diff --git a/src/components/activities/commons/AccessibleFormField.css b/src/components/activities/commons/AccessibleFormField.css index 02dddcf..7484149 100644 --- a/src/components/activities/commons/AccessibleFormField.css +++ b/src/components/activities/commons/AccessibleFormField.css @@ -119,12 +119,39 @@ } .form-field__button--secondary { - background-color: #ef4444; - color: #ffffff; + background-color: #e5e7eb; + color: #374151; } .form-field__button--secondary:hover:not(:disabled) { - background-color: #dc2626; + background-color: #d1d5db; +} + +.form-field__button--danger { + background-color: #fee2e2; + color: #dc2626; + border: 1px solid #fecaca; +} + +.form-field__button--danger:hover:not(:disabled) { + background-color: #fecaca; +} + +/* Answer group for multiple answers */ +.form-field__answer-group { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.form-field__alternatives { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.75rem; + background-color: #f9fafb; + border-radius: 0.375rem; + border: 1px solid #e5e7eb; } /* Card input styling */ diff --git a/src/components/activities/fill-in-the-blank/index.jsx b/src/components/activities/fill-in-the-blank/index.jsx index a0a678e..7220e0c 100644 --- a/src/components/activities/fill-in-the-blank/index.jsx +++ b/src/components/activities/fill-in-the-blank/index.jsx @@ -22,7 +22,11 @@ const FillBlankContext = React.createContext({ function InputInline({ answerIndex }) { const ctx = React.useContext(FillBlankContext); - const expectedLength = ctx.answers[answerIndex]?.length || 5; + const answer = ctx.answers[answerIndex]; + const answerLengths = Array.isArray(answer) ? answer.map((a) => a.length) : []; + const expectedLength = answerLengths.length > 0 + ? Math.max(...answerLengths, 5) + : (answer?.length || 5); const correct = ctx.correctness.length > 0 ? !!ctx.correctness[answerIndex] : null; let className = 'input-container'; if (correct === true) { @@ -134,9 +138,21 @@ function FillInTheBlank({ }), [userAnswers, answers, correctness, handleInputChange]); const checkAnswers = () => { - const results = userAnswers.map( - (answer, index) => (answer || '').toLowerCase() === (answers[index] || '').toLowerCase(), - ); + const emptyUserAnswers = userAnswers.filter((a) => !a || !a.trim()); + if (emptyUserAnswers.length > 0) { + toast.info('Silakan isi semua jawaban terlebih dahulu.', toastOption); + return; + } + + const results = userAnswers.map((userAnswer, index) => { + const expected = answers[index]; + const userAnswerLower = userAnswer.toLowerCase().trim(); + + if (Array.isArray(expected)) { + return expected.some((answer) => answer.toLowerCase().trim() === userAnswerLower); + } + return expected.toLowerCase().trim() === userAnswerLower; + }); setCorrectness(results); const isAllCorrect = results.every((result) => result); @@ -264,7 +280,9 @@ function FillInTheBlank({ // define prop-types FillInTheBlank.propTypes = { template: PropTypes.string.isRequired, - answers: PropTypes.arrayOf(PropTypes.string).isRequired, + answers: PropTypes.arrayOf( + PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]), + ).isRequired, storageKey: PropTypes.string, hint: PropTypes.string.isRequired, instructionText: PropTypes.string, diff --git a/src/pages/activities/fill-in-the-blanks/CreationPage.jsx b/src/pages/activities/fill-in-the-blanks/CreationPage.jsx index 02acf51..f1cdfc0 100644 --- a/src/pages/activities/fill-in-the-blanks/CreationPage.jsx +++ b/src/pages/activities/fill-in-the-blanks/CreationPage.jsx @@ -13,26 +13,39 @@ function FillInBlankCreationPage() { const [instruction, setInstruction] = useState('Lengkapi kalimat berikut dengan kata yang tepat.'); const [errors, setErrors] = useState({}); + const getTotalAnswerCount = () => answers.filter((a) => { + if (Array.isArray(a)) return a.length > 0; + return !!a; + }).length; + const validateTemplate = (value) => { const placeholders = value.match(/\{(\d+)\}/g) || []; - const numbers = placeholders.map(p => parseInt(p.match(/\d+/)[0])); + const numbers = placeholders.map((p) => parseInt(p.match(/\d+/)[0])); const maxNumber = Math.max(...numbers, 0); - + if (placeholders.length === 0 && value.trim()) { return 'Template harus mengandung setidaknya satu placeholder seperti {1}'; } - - if (maxNumber > answers.length) { - return `Template menggunakan placeholder {${maxNumber}} tetapi hanya ada ${answers.length} jawaban`; + + if (maxNumber > getTotalAnswerCount()) { + return `Template menggunakan placeholder {${maxNumber}} tetapi hanya ada ${getTotalAnswerCount()} jawaban`; } - + return ''; }; const validateAnswers = () => { - const emptyAnswers = answers.filter(answer => !answer.trim()).length; - if (emptyAnswers > 0) { - return `${emptyAnswers} jawaban masih kosong`; + let emptyPlaceholders = 0; + answers.forEach((answer) => { + if (Array.isArray(answer)) { + const hasValidAnswer = answer.some((a) => a.trim()); + if (!hasValidAnswer) emptyPlaceholders++; + } else if (!answer.trim()) { + emptyPlaceholders++; + } + }); + if (emptyPlaceholders > 0) { + return `${emptyPlaceholders} jawaban masih kosong`; } return ''; }; @@ -44,7 +57,7 @@ function FillInBlankCreationPage() { const removeAnswer = (index) => { const newAnswers = answers.filter((_, i) => i !== index); setAnswers(newAnswers); - + // Update template placeholders let newTemplate = template; for (let i = index + 1; i <= answers.length; i++) { @@ -55,33 +68,90 @@ function FillInBlankCreationPage() { const updateAnswer = (index, value) => { const newAnswers = [...answers]; - newAnswers[index] = value; + if (Array.isArray(newAnswers[index])) { + newAnswers[index] = [...newAnswers[index]]; + newAnswers[index][0] = value; + } else { + newAnswers[index] = value; + } + setAnswers(newAnswers); + }; + + const addAlternative = (index) => { + const newAnswers = [...answers]; + const current = newAnswers[index]; + if (Array.isArray(current)) { + newAnswers[index] = [...current, '']; + } else { + newAnswers[index] = [current, '']; + } + setAnswers(newAnswers); + }; + + const removeAlternative = (index, altIndex) => { + const newAnswers = [...answers]; + const current = newAnswers[index]; + if (Array.isArray(current)) { + const filtered = current.filter((_, i) => i !== altIndex); + newAnswers[index] = filtered.length === 1 ? filtered[0] : filtered; + } + setAnswers(newAnswers); + }; + + const updateAlternative = (index, altIndex, value) => { + const newAnswers = [...answers]; + if (Array.isArray(newAnswers[index])) { + newAnswers[index] = [...newAnswers[index]]; + newAnswers[index][altIndex] = value; + } setAnswers(newAnswers); }; const handleLoadFromBase64 = (data) => { if (data.template) setTemplate(data.template); - if (data.answers) setAnswers(data.answers.length > 0 ? data.answers : ['']); + if (data.answers) { + const isValidFormat = data.answers.every((answer) => { + if (typeof answer === 'string') return true; + if (Array.isArray(answer)) return answer.every((a) => typeof a === 'string'); + return false; + }); + if (isValidFormat) { + setAnswers(data.answers.length > 0 ? data.answers : ['']); + } + } if (data.hint) setHint(data.hint); if (data.instruction) setInstruction(data.instruction); }; useEffect(() => { const newErrors = {}; - + if (template) { const templateError = validateTemplate(template); if (templateError) newErrors.template = templateError; } - - if (answers.some(answer => answer.trim())) { + + if (answers.some((answer) => { + if (Array.isArray(answer)) return answer.some((a) => a.trim()); + return answer.trim(); + })) { const answersError = validateAnswers(); if (answersError) newErrors.answers = answersError; } - + setErrors(newErrors); }, [template, answers]); + const processAnswersForExport = () => answers.map((answer) => { + if (Array.isArray(answer)) { + return answer.filter((a) => a.trim()); + } + return answer.trim(); + }).filter((answer) => { + if (Array.isArray(answer)) return answer.length > 0; + return !!answer; + }); + const generateEmbedCode = () => { if (Object.keys(errors).length > 0) { alert('Harap perbaiki kesalahan sebelum generate embed code'); @@ -90,7 +160,7 @@ function FillInBlankCreationPage() { const rawData = { template, - answers: answers.filter(answer => answer.trim()), + answers: processAnswersForExport(), hint, storageKey: `fill-in-the-blank-${+new Date()}`, instruction, @@ -108,8 +178,8 @@ function FillInBlankCreationPage() {

Buat aktivitas isian kosong interaktif untuk pembelajaran

- @@ -125,7 +195,7 @@ function FillInBlankCreationPage() { error={errors.template} required rows={4} - showMarkdownToolbar={true} + showMarkdownToolbar />
@@ -135,36 +205,78 @@ function FillInBlankCreationPage() { {errors.answers} )} - + {answers.map((answer, index) => ( updateAnswer(index, e.target.value)} - placeholder={`Enter answer ${index + 1}`} required > -
- updateAnswer(index, e.target.value)} - placeholder={`Enter answer ${index + 1}`} - required - aria-label={`Answer for placeholder ${index + 1}`} - /> +
+ {Array.isArray(answer) ? ( +
+ {answer.map((alt, altIndex) => ( +
+ updateAlternative(index, altIndex, e.target.value)} + placeholder={`Alternative answer ${altIndex + 1}`} + aria-label={`Alternative answer ${altIndex + 1} for placeholder ${index + 1}`} + /> + {altIndex > 0 && ( + + )} +
+ ))} + +
+ ) : ( +
+ updateAnswer(index, e.target.value)} + placeholder={`Enter answer ${index + 1}`} + required + aria-label={`Answer for placeholder ${index + 1}`} + /> + +
+ )} {answers.length > 1 && ( )}
@@ -193,18 +305,18 @@ function FillInBlankCreationPage() { />
- - -