diff --git a/lib/app/const/enums/question_type.dart b/lib/app/const/enums/question_type.dart new file mode 100644 index 0000000..d61ca6f --- /dev/null +++ b/lib/app/const/enums/question_type.dart @@ -0,0 +1 @@ +enum QuestionType { fillTheBlank, option, trueOrFalse } diff --git a/lib/data/models/quiz/quiestion_data_model.dart b/lib/data/models/quiz/quiestion_data_model.dart new file mode 100644 index 0000000..9e37bd7 --- /dev/null +++ b/lib/data/models/quiz/quiestion_data_model.dart @@ -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? 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? 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, + ); + } +} diff --git a/lib/feature/quiz_creation/controller/quiz_creation_controller.dart b/lib/feature/quiz_creation/controller/quiz_creation_controller.dart index 04623e6..1df9d85 100644 --- a/lib/feature/quiz_creation/controller/quiz_creation_controller.dart +++ b/lib/feature/quiz_creation/controller/quiz_creation_controller.dart @@ -1,17 +1,126 @@ import 'package:flutter/widgets.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 { - TextEditingController questionTC = TextEditingController(); - TextEditingController answerTC = TextEditingController(); + final TextEditingController questionTC = TextEditingController(); + final TextEditingController answerTC = TextEditingController(); + final List optionTCList = List.generate(4, (_) => TextEditingController()); + final RxInt selectedOptionIndex = 0.obs; RxBool isGenerate = true.obs; - Rx currentQuestionType = QuestionType.fillTheBlank.obs; + RxList quizData = [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(currentQuestionType, (type) { + if (quizData.isNotEmpty) { + _updateCurrentQuestion(type: type); + } + }); + + // Listener perubahan jawaban benar (untuk pilihan ganda) + ever(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? 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 } diff --git a/lib/feature/quiz_creation/view/component/custom_question_component.dart b/lib/feature/quiz_creation/view/component/custom_question_component.dart index 0c02854..2e083d9 100644 --- a/lib/feature/quiz_creation/view/component/custom_question_component.dart +++ b/lib/feature/quiz_creation/view/component/custom_question_component.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.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/view/component/fill_the_blank_component.dart'; import 'package:quiz_app/feature/quiz_creation/view/component/option_question_component.dart'; @@ -33,10 +34,7 @@ class CustomQuestionComponent extends GetView { answerTC: controller.answerTC, ); case QuestionType.option: - return OptionQuestionComponent( - questionTC: TextEditingController(), - optionTCList: List.generate(4, (index) => TextEditingController()), - ); + return OptionQuestionComponent(); case QuestionType.trueOrFalse: return TrueFalseQuestionComponent(questionTC: controller.questionTC); } @@ -44,35 +42,66 @@ class CustomQuestionComponent extends GetView { } Widget _buildNumberPicker() { - return Wrap( - spacing: 8, - runSpacing: 8, - children: List.generate(14, (index) { - return Container( - width: 42, - height: 42, - alignment: Alignment.center, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: AppColors.borderLight), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 4, - offset: const Offset(2, 2), - ), - ], + return Obx( + () => SizedBox( + height: 100, + child: GridView.builder( + scrollDirection: Axis.horizontal, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + childAspectRatio: 1, ), - child: Text( - '${index + 1}', - style: const TextStyle( - fontWeight: FontWeight.w600, - color: AppColors.darkText, - ), - ), - ); - }), + 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( + width: 60, + height: 60, + alignment: Alignment.center, + decoration: BoxDecoration( + color: isSelected ? AppColors.primaryBlue : Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isSelected ? AppColors.primaryBlue : AppColors.borderLight, + width: 2, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(13), + blurRadius: 4, + offset: const Offset(2, 2), + ), + ], + ), + child: isLast + ? const Icon(Icons.add, color: AppColors.darkText) + : Text( + '${controller.quizData[index].index}', + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + color: isSelected ? Colors.white : AppColors.darkText, + ), + ), + ); + }), + ); + }, + ), + ), ); } diff --git a/lib/feature/quiz_creation/view/component/option_question_component.dart b/lib/feature/quiz_creation/view/component/option_question_component.dart index 1e46db0..0a95424 100644 --- a/lib/feature/quiz_creation/view/component/option_question_component.dart +++ b/lib/feature/quiz_creation/view/component/option_question_component.dart @@ -1,23 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:quiz_app/component/global_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 { - final TextEditingController questionTC; - final List optionTCList; - - const OptionQuestionComponent({ - super.key, - required this.questionTC, - required this.optionTCList, - }); - - @override - State createState() => _OptionQuestionComponentState(); -} - -class _OptionQuestionComponentState extends State { - String? selectedCorrectAnswer; // A, B, C, D +class OptionQuestionComponent extends GetView { + const OptionQuestionComponent({super.key}); @override Widget build(BuildContext context) { @@ -26,19 +14,19 @@ class _OptionQuestionComponentState extends State { // Pertanyaan LabelTextField(label: "Pertanyaan"), GlobalTextField( - controller: widget.questionTC, + controller: controller.questionTC, limitTextLine: 3, hintText: "Tulis Pertanyaan", ), const SizedBox(height: 15), // Pilihan A, B, C, D - ...List.generate(widget.optionTCList.length, (index) { + ...List.generate(controller.optionTCList.length, (index) { return Column( children: [ LabelTextField(label: "Pilihan ${String.fromCharCode(65 + index)}"), GlobalTextField( - controller: widget.optionTCList[index], + controller: controller.optionTCList[index], hintText: "Tulis Pilihan ${String.fromCharCode(65 + index)}", ), const SizedBox(height: 10), @@ -46,8 +34,9 @@ class _OptionQuestionComponentState extends State { ); }), - // Jawaban Benar Dropdown const SizedBox(height: 10), + + // Jawaban Benar Dropdown LabelTextField(label: "Jawaban Benar"), Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), @@ -56,23 +45,24 @@ class _OptionQuestionComponentState extends State { borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey), ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - value: selectedCorrectAnswer, - hint: const Text('Pilih Jawaban Benar'), - isExpanded: true, - items: List.generate(widget.optionTCList.length, (index) { - final optionLabel = String.fromCharCode(65 + index); // 'A', 'B', 'C', etc. - return DropdownMenuItem( - value: optionLabel, - child: Text(optionLabel), - ); - }), - onChanged: (value) { - setState(() { - selectedCorrectAnswer = value; - }); - }, + child: Obx( + () => DropdownButtonHideUnderline( + child: DropdownButton( + value: controller.selectedOptionIndex.value, + isExpanded: true, + items: List.generate(controller.optionTCList.length, (index) { + final optionLabel = String.fromCharCode(65 + index); // 'A', 'B', 'C', etc. + return DropdownMenuItem( + value: index, + child: Text(optionLabel), + ); + }), + onChanged: (value) { + if (value != null) { + controller.selectedOptionIndex.value = value; + } + }, + ), ), ), ), diff --git a/lib/feature/quiz_creation/view/component/true_or_false_component.dart b/lib/feature/quiz_creation/view/component/true_or_false_component.dart index 7523842..79f7170 100644 --- a/lib/feature/quiz_creation/view/component/true_or_false_component.dart +++ b/lib/feature/quiz_creation/view/component/true_or_false_component.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:quiz_app/component/global_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 { final TextEditingController questionTC; const TrueFalseQuestionComponent({ @@ -10,13 +12,6 @@ class TrueFalseQuestionComponent extends StatefulWidget { required this.questionTC, }); - @override - State createState() => _TrueFalseQuestionComponentState(); -} - -class _TrueFalseQuestionComponentState extends State { - bool? selectedAnswer; // true or false - @override Widget build(BuildContext context) { return Column( @@ -24,13 +19,13 @@ class _TrueFalseQuestionComponentState extends State // Pertanyaan LabelTextField(label: "Pertanyaan"), GlobalTextField( - controller: widget.questionTC, + controller: questionTC, limitTextLine: 3, hintText: "Tulis Pertanyaan", ), const SizedBox(height: 15), - // Jawaban Dropdown + // Jawaban Benar Dropdown LabelTextField(label: "Jawaban Benar"), Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), @@ -39,26 +34,28 @@ class _TrueFalseQuestionComponentState extends State borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey), ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - value: selectedAnswer, - hint: const Text('Pilih Jawaban Benar'), - isExpanded: true, - items: const [ - DropdownMenuItem( - value: true, - child: Text('True'), - ), - DropdownMenuItem( - value: false, - child: Text('False'), - ), - ], - onChanged: (value) { - setState(() { - selectedAnswer = value; - }); - }, + child: Obx( + () => DropdownButtonHideUnderline( + child: DropdownButton( + value: _getCurrentAnswer(), + hint: const Text('Pilih Jawaban Benar'), + isExpanded: true, + items: const [ + DropdownMenuItem( + value: true, + child: Text('True'), + ), + DropdownMenuItem( + value: false, + child: Text('False'), + ), + ], + onChanged: (value) { + if (value != null) { + controller.updateTOFAnswer(value); + } + }, + ), ), ), ), @@ -67,4 +64,12 @@ class _TrueFalseQuestionComponentState extends State ], ); } + + 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; + } } diff --git a/lib/feature/quiz_creation/view/quiz_creation_view.dart b/lib/feature/quiz_creation/view/quiz_creation_view.dart index eede0b6..6c2076c 100644 --- a/lib/feature/quiz_creation/view/quiz_creation_view.dart +++ b/lib/feature/quiz_creation/view/quiz_creation_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.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/view/component/custom_question_component.dart'; import 'package:quiz_app/feature/quiz_creation/view/component/generate_component.dart'; @@ -40,6 +41,8 @@ class QuizCreationView extends GetView { Obx( () => controller.isGenerate.value ? GenerateComponent() : CustomQuestionComponent(), ), + const SizedBox(height: 30), + GlobalButton(text: "simpan semua", onPressed: () {}) ], ), ),