feat: logic for quiz input is done

This commit is contained in:
akhdanre 2025-04-27 13:40:10 +07:00
parent 05a22f3360
commit e801962e4c
7 changed files with 286 additions and 106 deletions

View File

@ -0,0 +1 @@
enum QuestionType { fillTheBlank, option, trueOrFalse }

View File

@ -0,0 +1,43 @@
import 'package:quiz_app/app/const/enums/question_type.dart';
class OptionData {
final int index;
final String text;
OptionData({required this.index, required this.text});
}
class QuestionData {
final int index;
final String? question;
final String? answer;
final List<OptionData>? options;
final int? correctAnswerIndex;
final QuestionType? type;
QuestionData({
required this.index,
this.question,
this.answer,
this.options,
this.correctAnswerIndex,
this.type,
});
QuestionData copyWith({
String? question,
String? answer,
List<OptionData>? options,
int? correctAnswerIndex,
QuestionType? type,
}) {
return QuestionData(
index: index,
question: question ?? this.question,
answer: answer ?? this.answer,
options: options ?? this.options,
correctAnswerIndex: correctAnswerIndex ?? this.correctAnswerIndex,
type: type ?? this.type,
);
}
}

View File

@ -1,17 +1,126 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:quiz_app/app/const/enums/question_type.dart';
import 'package:quiz_app/data/models/quiz/quiestion_data_model.dart';
class QuizCreationController extends GetxController { class QuizCreationController extends GetxController {
TextEditingController questionTC = TextEditingController(); final TextEditingController questionTC = TextEditingController();
TextEditingController answerTC = TextEditingController(); final TextEditingController answerTC = TextEditingController();
final List<TextEditingController> optionTCList = List.generate(4, (_) => TextEditingController());
final RxInt selectedOptionIndex = 0.obs;
RxBool isGenerate = true.obs; RxBool isGenerate = true.obs;
Rx<QuestionType> currentQuestionType = QuestionType.fillTheBlank.obs; Rx<QuestionType> currentQuestionType = QuestionType.fillTheBlank.obs;
RxList<QuestionData> quizData = <QuestionData>[QuestionData(index: 1)].obs;
RxInt selectedQuizIndex = 0.obs;
onCreationTypeChange(bool value) => isGenerate.value = value; @override
void onInit() {
super.onInit();
_initializeListeners();
}
onQuestionTypeChange(QuestionType type) => currentQuestionType.value = type; void _initializeListeners() {
// Listener untuk pertanyaan
questionTC.addListener(() {
if (quizData.isNotEmpty) {
_updateCurrentQuestion(question: questionTC.text);
}
});
// Listener untuk jawaban langsung (Fill the Blank atau True/False)
answerTC.addListener(() {
if (quizData.isNotEmpty && currentQuestionType.value != QuestionType.option) {
_updateCurrentQuestion(answer: answerTC.text);
}
});
// Listener untuk masing-masing pilihan opsi
for (var i = 0; i < optionTCList.length; i++) {
optionTCList[i].addListener(() {
if (quizData.isNotEmpty && currentQuestionType.value == QuestionType.option) {
_updateCurrentQuestion(
options: List.generate(
optionTCList.length,
(index) => OptionData(index: index, text: optionTCList[index].text),
),
);
}
});
}
// Listener perubahan tipe soal
ever<QuestionType>(currentQuestionType, (type) {
if (quizData.isNotEmpty) {
_updateCurrentQuestion(type: type);
}
});
// Listener perubahan jawaban benar (untuk pilihan ganda)
ever<int>(selectedOptionIndex, (index) {
if (quizData.isNotEmpty && currentQuestionType.value == QuestionType.option) {
_updateCurrentQuestion(correctAnswerIndex: index);
}
});
}
void onCreationTypeChange(bool value) {
isGenerate.value = value;
}
void onQuestionTypeChange(QuestionType type) {
currentQuestionType.value = type;
}
void onQuestionAdd() {
quizData.add(QuestionData(index: quizData.length + 1));
}
void onSelectedQuizItem(int index) {
selectedQuizIndex.value = index;
final data = quizData[index];
questionTC.text = data.question ?? "";
answerTC.text = data.answer ?? "";
currentQuestionType.value = data.type ?? QuestionType.fillTheBlank;
if (data.options != null && data.options!.isNotEmpty) {
for (int i = 0; i < optionTCList.length; i++) {
optionTCList[i].text = data.options!.length > i ? data.options![i].text : '';
}
selectedOptionIndex.value = data.correctAnswerIndex ?? 0;
} else {
for (var controller in optionTCList) {
controller.clear();
}
selectedOptionIndex.value = 0;
}
}
void _updateCurrentQuestion({
String? question,
String? answer,
List<OptionData>? options,
int? correctAnswerIndex,
QuestionType? type,
}) {
final current = quizData[selectedQuizIndex.value];
quizData[selectedQuizIndex.value] = current.copyWith(
question: question,
answer: answer,
options: options ??
(currentQuestionType.value == QuestionType.option
? List.generate(
optionTCList.length,
(index) => OptionData(index: index, text: optionTCList[index].text),
)
: null),
correctAnswerIndex: correctAnswerIndex,
type: type,
);
}
void updateTOFAnswer(bool answer) {
_updateCurrentQuestion(answer: answer.toString());
}
} }
enum QuestionType { fillTheBlank, option, trueOrFalse }

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:quiz_app/app/const/colors/app_colors.dart'; import 'package:quiz_app/app/const/colors/app_colors.dart';
import 'package:quiz_app/app/const/enums/question_type.dart';
import 'package:quiz_app/feature/quiz_creation/controller/quiz_creation_controller.dart'; import 'package:quiz_app/feature/quiz_creation/controller/quiz_creation_controller.dart';
import 'package:quiz_app/feature/quiz_creation/view/component/fill_the_blank_component.dart'; import 'package:quiz_app/feature/quiz_creation/view/component/fill_the_blank_component.dart';
import 'package:quiz_app/feature/quiz_creation/view/component/option_question_component.dart'; import 'package:quiz_app/feature/quiz_creation/view/component/option_question_component.dart';
@ -33,10 +34,7 @@ class CustomQuestionComponent extends GetView<QuizCreationController> {
answerTC: controller.answerTC, answerTC: controller.answerTC,
); );
case QuestionType.option: case QuestionType.option:
return OptionQuestionComponent( return OptionQuestionComponent();
questionTC: TextEditingController(),
optionTCList: List.generate(4, (index) => TextEditingController()),
);
case QuestionType.trueOrFalse: case QuestionType.trueOrFalse:
return TrueFalseQuestionComponent(questionTC: controller.questionTC); return TrueFalseQuestionComponent(questionTC: controller.questionTC);
} }
@ -44,36 +42,67 @@ class CustomQuestionComponent extends GetView<QuizCreationController> {
} }
Widget _buildNumberPicker() { Widget _buildNumberPicker() {
return Wrap( return Obx(
spacing: 8, () => SizedBox(
runSpacing: 8, height: 100,
children: List.generate(14, (index) { child: GridView.builder(
scrollDirection: Axis.horizontal,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 1,
),
itemCount: controller.quizData.length + 1,
itemBuilder: (context, index) {
final isLast = index == controller.quizData.length;
return GestureDetector(
onTap: () {
if (isLast) {
controller.onQuestionAdd();
} else {
controller.onSelectedQuizItem(index);
}
},
child: Obx(() {
bool isSelected = controller.selectedQuizIndex.value == index;
return Container( return Container(
width: 42, width: 60,
height: 42, height: 60,
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: isSelected ? AppColors.primaryBlue : Colors.white,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.borderLight), border: Border.all(
color: isSelected ? AppColors.primaryBlue : AppColors.borderLight,
width: 2,
),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.05), color: Colors.black.withAlpha(13),
blurRadius: 4, blurRadius: 4,
offset: const Offset(2, 2), offset: const Offset(2, 2),
), ),
], ],
), ),
child: Text( child: isLast
'${index + 1}', ? const Icon(Icons.add, color: AppColors.darkText)
style: const TextStyle( : Text(
'${controller.quizData[index].index}',
style: TextStyle(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppColors.darkText, fontSize: 16,
color: isSelected ? Colors.white : AppColors.darkText,
), ),
), ),
); );
}), }),
); );
},
),
),
);
} }
Widget _buildQuizTypeSelector() { Widget _buildQuizTypeSelector() {

View File

@ -1,23 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:quiz_app/component/global_text_field.dart'; import 'package:quiz_app/component/global_text_field.dart';
import 'package:quiz_app/component/label_text_field.dart'; import 'package:quiz_app/component/label_text_field.dart';
import 'package:quiz_app/feature/quiz_creation/controller/quiz_creation_controller.dart'; // Pastikan import controllermu
class OptionQuestionComponent extends StatefulWidget { class OptionQuestionComponent extends GetView<QuizCreationController> {
final TextEditingController questionTC; const OptionQuestionComponent({super.key});
final List<TextEditingController> optionTCList;
const OptionQuestionComponent({
super.key,
required this.questionTC,
required this.optionTCList,
});
@override
State<OptionQuestionComponent> createState() => _OptionQuestionComponentState();
}
class _OptionQuestionComponentState extends State<OptionQuestionComponent> {
String? selectedCorrectAnswer; // A, B, C, D
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -26,19 +14,19 @@ class _OptionQuestionComponentState extends State<OptionQuestionComponent> {
// Pertanyaan // Pertanyaan
LabelTextField(label: "Pertanyaan"), LabelTextField(label: "Pertanyaan"),
GlobalTextField( GlobalTextField(
controller: widget.questionTC, controller: controller.questionTC,
limitTextLine: 3, limitTextLine: 3,
hintText: "Tulis Pertanyaan", hintText: "Tulis Pertanyaan",
), ),
const SizedBox(height: 15), const SizedBox(height: 15),
// Pilihan A, B, C, D // Pilihan A, B, C, D
...List.generate(widget.optionTCList.length, (index) { ...List.generate(controller.optionTCList.length, (index) {
return Column( return Column(
children: [ children: [
LabelTextField(label: "Pilihan ${String.fromCharCode(65 + index)}"), LabelTextField(label: "Pilihan ${String.fromCharCode(65 + index)}"),
GlobalTextField( GlobalTextField(
controller: widget.optionTCList[index], controller: controller.optionTCList[index],
hintText: "Tulis Pilihan ${String.fromCharCode(65 + index)}", hintText: "Tulis Pilihan ${String.fromCharCode(65 + index)}",
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
@ -46,8 +34,9 @@ class _OptionQuestionComponentState extends State<OptionQuestionComponent> {
); );
}), }),
// Jawaban Benar Dropdown
const SizedBox(height: 10), const SizedBox(height: 10),
// Jawaban Benar Dropdown
LabelTextField(label: "Jawaban Benar"), LabelTextField(label: "Jawaban Benar"),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
@ -56,26 +45,27 @@ class _OptionQuestionComponentState extends State<OptionQuestionComponent> {
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey), border: Border.all(color: Colors.grey),
), ),
child: DropdownButtonHideUnderline( child: Obx(
child: DropdownButton<String>( () => DropdownButtonHideUnderline(
value: selectedCorrectAnswer, child: DropdownButton<int>(
hint: const Text('Pilih Jawaban Benar'), value: controller.selectedOptionIndex.value,
isExpanded: true, isExpanded: true,
items: List.generate(widget.optionTCList.length, (index) { items: List.generate(controller.optionTCList.length, (index) {
final optionLabel = String.fromCharCode(65 + index); // 'A', 'B', 'C', etc. final optionLabel = String.fromCharCode(65 + index); // 'A', 'B', 'C', etc.
return DropdownMenuItem<String>( return DropdownMenuItem<int>(
value: optionLabel, value: index,
child: Text(optionLabel), child: Text(optionLabel),
); );
}), }),
onChanged: (value) { onChanged: (value) {
setState(() { if (value != null) {
selectedCorrectAnswer = value; controller.selectedOptionIndex.value = value;
}); }
}, },
), ),
), ),
), ),
),
const SizedBox(height: 20), const SizedBox(height: 20),
], ],

View File

@ -1,8 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:quiz_app/component/global_text_field.dart'; import 'package:quiz_app/component/global_text_field.dart';
import 'package:quiz_app/component/label_text_field.dart'; import 'package:quiz_app/component/label_text_field.dart';
import 'package:quiz_app/feature/quiz_creation/controller/quiz_creation_controller.dart'; // Ganti path sesuai projekmu
class TrueFalseQuestionComponent extends StatefulWidget { class TrueFalseQuestionComponent extends GetView<QuizCreationController> {
final TextEditingController questionTC; final TextEditingController questionTC;
const TrueFalseQuestionComponent({ const TrueFalseQuestionComponent({
@ -10,13 +12,6 @@ class TrueFalseQuestionComponent extends StatefulWidget {
required this.questionTC, required this.questionTC,
}); });
@override
State<TrueFalseQuestionComponent> createState() => _TrueFalseQuestionComponentState();
}
class _TrueFalseQuestionComponentState extends State<TrueFalseQuestionComponent> {
bool? selectedAnswer; // true or false
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
@ -24,13 +19,13 @@ class _TrueFalseQuestionComponentState extends State<TrueFalseQuestionComponent>
// Pertanyaan // Pertanyaan
LabelTextField(label: "Pertanyaan"), LabelTextField(label: "Pertanyaan"),
GlobalTextField( GlobalTextField(
controller: widget.questionTC, controller: questionTC,
limitTextLine: 3, limitTextLine: 3,
hintText: "Tulis Pertanyaan", hintText: "Tulis Pertanyaan",
), ),
const SizedBox(height: 15), const SizedBox(height: 15),
// Jawaban Dropdown // Jawaban Benar Dropdown
LabelTextField(label: "Jawaban Benar"), LabelTextField(label: "Jawaban Benar"),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
@ -39,9 +34,10 @@ class _TrueFalseQuestionComponentState extends State<TrueFalseQuestionComponent>
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey), border: Border.all(color: Colors.grey),
), ),
child: DropdownButtonHideUnderline( child: Obx(
() => DropdownButtonHideUnderline(
child: DropdownButton<bool>( child: DropdownButton<bool>(
value: selectedAnswer, value: _getCurrentAnswer(),
hint: const Text('Pilih Jawaban Benar'), hint: const Text('Pilih Jawaban Benar'),
isExpanded: true, isExpanded: true,
items: const [ items: const [
@ -55,16 +51,25 @@ class _TrueFalseQuestionComponentState extends State<TrueFalseQuestionComponent>
), ),
], ],
onChanged: (value) { onChanged: (value) {
setState(() { if (value != null) {
selectedAnswer = value; controller.updateTOFAnswer(value);
}); }
}, },
), ),
), ),
), ),
),
const SizedBox(height: 20), const SizedBox(height: 20),
], ],
); );
} }
bool? _getCurrentAnswer() {
// Ambil answer dari controller dan parsing ke bool
final currentAnswer = controller.quizData[controller.selectedQuizIndex.value].answer;
if (currentAnswer == "true") return true;
if (currentAnswer == "false") return false;
return null;
}
} }

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:quiz_app/app/const/colors/app_colors.dart'; import 'package:quiz_app/app/const/colors/app_colors.dart';
import 'package:quiz_app/component/global_button.dart';
import 'package:quiz_app/feature/quiz_creation/controller/quiz_creation_controller.dart'; import 'package:quiz_app/feature/quiz_creation/controller/quiz_creation_controller.dart';
import 'package:quiz_app/feature/quiz_creation/view/component/custom_question_component.dart'; import 'package:quiz_app/feature/quiz_creation/view/component/custom_question_component.dart';
import 'package:quiz_app/feature/quiz_creation/view/component/generate_component.dart'; import 'package:quiz_app/feature/quiz_creation/view/component/generate_component.dart';
@ -40,6 +41,8 @@ class QuizCreationView extends GetView<QuizCreationController> {
Obx( Obx(
() => controller.isGenerate.value ? GenerateComponent() : CustomQuestionComponent(), () => controller.isGenerate.value ? GenerateComponent() : CustomQuestionComponent(),
), ),
const SizedBox(height: 30),
GlobalButton(text: "simpan semua", onPressed: () {})
], ],
), ),
), ),