diff --git a/lib/component/widget/question_container_widget.dart b/lib/component/widget/question_container_widget.dart new file mode 100644 index 0000000..9cd75e4 --- /dev/null +++ b/lib/component/widget/question_container_widget.dart @@ -0,0 +1,194 @@ +import 'package:flutter/material.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/data/models/quiz/quiestion_data_model.dart'; +import 'package:quiz_app/feature/quiz_play/controller/quiz_play_controller.dart'; + +class QuestionContainerWidget extends StatelessWidget { + final QuestionData question; + final AnsweredQuestion? answeredQuestion; + + const QuestionContainerWidget({super.key, required this.question, this.answeredQuestion}); + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + margin: const EdgeInsets.only(bottom: 20), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.borderLight), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 6, + offset: const Offset(2, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Soal ${question.index}', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: AppColors.darkText)), + const SizedBox(height: 6), + Text( + _mapQuestionTypeToText(question.type), + style: const TextStyle(fontSize: 12, color: AppColors.softGrayText, fontStyle: FontStyle.italic), + ), + const SizedBox(height: 12), + Text( + question.question ?? '-', + style: const TextStyle(fontSize: 16, color: AppColors.darkText), + ), + const SizedBox(height: 16), + _buildAnswerSection(question), + if (answeredQuestion != null) ...[ + const SizedBox(height: 16), + _buildAnsweredSection(answeredQuestion!), + ], + const SizedBox(height: 10), + const Text( + 'Durasi: 0 detik', + style: TextStyle(fontSize: 14, color: AppColors.softGrayText), + ), + ], + ), + ); + } + + Widget _buildAnswerSection(QuestionData question) { + if (question.type == QuestionType.option && question.options != null) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: question.options!.map((option) { + bool isCorrect = question.correctAnswerIndex == option.index; + return Container( + margin: const EdgeInsets.symmetric(vertical: 4), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + decoration: BoxDecoration( + color: isCorrect ? AppColors.primaryBlue.withOpacity(0.1) : Colors.transparent, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Icon( + isCorrect ? Icons.check_circle_rounded : Icons.circle_outlined, + size: 18, + color: isCorrect ? AppColors.primaryBlue : AppColors.softGrayText, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + option.text, + style: TextStyle( + fontWeight: isCorrect ? FontWeight.bold : FontWeight.normal, + color: isCorrect ? AppColors.primaryBlue : AppColors.darkText, + ), + ), + ), + ], + ), + ); + }).toList(), + ); + } else if (question.type == QuestionType.fillTheBlank) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildFillTheBlankPossibilities(question.answer ?? '-'), + ); + } else if (question.type == QuestionType.trueOrFalse) { + return Text( + 'Jawaban: ${question.answer ?? '-'}', + style: const TextStyle(color: AppColors.softGrayText), + ); + } else { + return const SizedBox(); + } + } + + Widget _buildAnsweredSection(AnsweredQuestion answered) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Jawaban Anda:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: AppColors.darkText)), + const SizedBox(height: 6), + Row( + children: [ + Icon( + answered.isCorrect ? Icons.check_circle : Icons.cancel, + color: answered.isCorrect ? Colors.green : Colors.red, + size: 20, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + answered.selectedAnswer, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: answered.isCorrect ? Colors.green : Colors.red, + ), + ), + ), + ], + ), + ], + ); + } + + String _mapQuestionTypeToText(QuestionType? type) { + switch (type) { + case QuestionType.option: + return 'Tipe: Pilihan Ganda'; + case QuestionType.fillTheBlank: + return 'Tipe: Isian Kosong'; + case QuestionType.trueOrFalse: + return 'Tipe: Benar / Salah'; + default: + return 'Tipe: Tidak diketahui'; + } + } + + List _buildFillTheBlankPossibilities(String answer) { + List possibilities = [ + _capitalizeEachWord(answer), + answer.toLowerCase(), + _capitalizeFirstWordOnly(answer), + ]; + + return possibilities.map((option) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Row( + children: [ + const Icon(Icons.arrow_right, size: 18, color: AppColors.softGrayText), + const SizedBox(width: 6), + Text( + option, + style: const TextStyle(color: AppColors.darkText), + ), + ], + ), + ); + }).toList(); + } + + String _capitalizeEachWord(String text) { + return text.split(' ').map((word) { + if (word.isEmpty) return word; + return word[0].toUpperCase() + word.substring(1).toLowerCase(); + }).join(' '); + } + + String _capitalizeFirstWordOnly(String text) { + if (text.isEmpty) return text; + List parts = text.split(' '); + parts[0] = parts[0][0].toUpperCase() + parts[0].substring(1).toLowerCase(); + for (int i = 1; i < parts.length; i++) { + parts[i] = parts[i].toLowerCase(); + } + return parts.join(' '); + } +} diff --git a/lib/feature/quiz_play/controller/quiz_play_controller.dart b/lib/feature/quiz_play/controller/quiz_play_controller.dart index 4e23248..ce1a4cd 100644 --- a/lib/feature/quiz_play/controller/quiz_play_controller.dart +++ b/lib/feature/quiz_play/controller/quiz_play_controller.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:quiz_app/app/routes/app_pages.dart'; -import 'package:quiz_app/core/utils/logger.dart'; import 'package:quiz_app/data/models/quiz/library_quiz_model.dart'; import 'package:quiz_app/data/models/quiz/question_listings_model.dart'; @@ -136,7 +135,7 @@ class QuizPlayController extends GetxController { // ); Get.toNamed( AppRoutes.resultQuizPage, - arguments: [quizData.questionListings, answeredQuestions], + arguments: [quizData, answeredQuestions], ); } diff --git a/lib/feature/quiz_preview/controller/quiz_preview_controller.dart b/lib/feature/quiz_preview/controller/quiz_preview_controller.dart index d6b13d1..0bfa96c 100644 --- a/lib/feature/quiz_preview/controller/quiz_preview_controller.dart +++ b/lib/feature/quiz_preview/controller/quiz_preview_controller.dart @@ -1,6 +1,5 @@ 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/app/routes/app_pages.dart'; import 'package:quiz_app/core/utils/logger.dart'; @@ -106,153 +105,6 @@ class QuizPreviewController extends GetxController { }).toList(); } - Widget buildQuestionCard(QuestionData question) { - return Container( - width: double.infinity, - margin: const EdgeInsets.only(bottom: 20), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.borderLight), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 6, - offset: const Offset(2, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Soal ${question.index}', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: AppColors.darkText)), - const SizedBox(height: 6), - Text( - _mapQuestionTypeToText(question.type), - style: const TextStyle(fontSize: 12, color: AppColors.softGrayText, fontStyle: FontStyle.italic), - ), - const SizedBox(height: 12), - Text( - question.question ?? '-', - style: const TextStyle(fontSize: 16, color: AppColors.darkText), - ), - const SizedBox(height: 16), - _buildAnswerSection(question), - const SizedBox(height: 10), - const Text( - 'Durasi: 0 detik', - style: TextStyle(fontSize: 14, color: AppColors.softGrayText), - ), - ], - ), - ); - } - - Widget _buildAnswerSection(QuestionData question) { - if (question.type == QuestionType.option && question.options != null) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: question.options!.map((option) { - bool isCorrect = question.correctAnswerIndex == option.index; - return Container( - margin: const EdgeInsets.symmetric(vertical: 4), - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), - decoration: BoxDecoration( - color: isCorrect ? AppColors.primaryBlue.withOpacity(0.1) : Colors.transparent, - borderRadius: BorderRadius.circular(8), - ), - child: Row( - children: [ - Icon( - isCorrect ? Icons.check_circle_rounded : Icons.circle_outlined, - size: 18, - color: isCorrect ? AppColors.primaryBlue : AppColors.softGrayText, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - option.text, - style: TextStyle( - fontWeight: isCorrect ? FontWeight.bold : FontWeight.normal, - color: isCorrect ? AppColors.primaryBlue : AppColors.darkText, - ), - ), - ), - ], - ), - ); - }).toList(), - ); - } else if (question.type == QuestionType.fillTheBlank) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: _buildFillTheBlankPossibilities(question.answer ?? '-'), - ); - } else if (question.type == QuestionType.trueOrFalse) { - return Text( - 'Jawaban: ${question.answer ?? '-'}', - style: const TextStyle(color: AppColors.softGrayText), - ); - } else { - return const SizedBox(); - } - } - - String _mapQuestionTypeToText(QuestionType? type) { - switch (type) { - case QuestionType.option: - return 'Tipe: Pilihan Ganda'; - case QuestionType.fillTheBlank: - return 'Tipe: Isian Kosong'; - case QuestionType.trueOrFalse: - return 'Tipe: Benar / Salah'; - default: - return 'Tipe: Tidak diketahui'; - } - } - - List _buildFillTheBlankPossibilities(String answer) { - List possibilities = [ - _capitalizeEachWord(answer), - answer.toLowerCase(), - _capitalizeFirstWordOnly(answer), - ]; - - return possibilities.map((option) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 2), - child: Row( - children: [ - const Icon(Icons.arrow_right, size: 18, color: AppColors.softGrayText), - const SizedBox(width: 6), - Text( - option, - style: const TextStyle(color: AppColors.darkText), - ), - ], - ), - ); - }).toList(); - } - - String _capitalizeEachWord(String text) { - return text.split(' ').map((word) { - if (word.isEmpty) return word; - return word[0].toUpperCase() + word.substring(1).toLowerCase(); - }).join(' '); - } - - String _capitalizeFirstWordOnly(String text) { - if (text.isEmpty) return text; - List parts = text.split(' '); - parts[0] = parts[0][0].toUpperCase() + parts[0].substring(1).toLowerCase(); - for (int i = 1; i < parts.length; i++) { - parts[i] = parts[i].toLowerCase(); - } - return parts.join(' '); - } - @override void onClose() { titleController.dispose(); diff --git a/lib/feature/quiz_preview/view/quiz_preview.dart b/lib/feature/quiz_preview/view/quiz_preview.dart index 48b721c..b50401c 100644 --- a/lib/feature/quiz_preview/view/quiz_preview.dart +++ b/lib/feature/quiz_preview/view/quiz_preview.dart @@ -4,6 +4,7 @@ import 'package:quiz_app/app/const/colors/app_colors.dart'; import 'package:quiz_app/component/global_button.dart'; import 'package:quiz_app/component/global_text_field.dart'; import 'package:quiz_app/component/label_text_field.dart'; +import 'package:quiz_app/component/widget/question_container_widget.dart'; import 'package:quiz_app/feature/quiz_preview/controller/quiz_preview_controller.dart'; class QuizPreviewPage extends GetView { @@ -61,7 +62,7 @@ class QuizPreviewPage extends GetView { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: controller.data.map((question) { - return controller.buildQuestionCard(question); + return QuestionContainerWidget(question: question); }).toList(), ); } diff --git a/lib/feature/quiz_result/controller/quiz_result_controller.dart b/lib/feature/quiz_result/controller/quiz_result_controller.dart index 7bce6ca..c6c7884 100644 --- a/lib/feature/quiz_result/controller/quiz_result_controller.dart +++ b/lib/feature/quiz_result/controller/quiz_result_controller.dart @@ -1,8 +1,12 @@ import 'package:get/get.dart'; +import 'package:quiz_app/app/const/enums/question_type.dart'; +import 'package:quiz_app/data/models/quiz/library_quiz_model.dart'; import 'package:quiz_app/data/models/quiz/question_listings_model.dart'; +import 'package:quiz_app/data/models/quiz/quiestion_data_model.dart'; import 'package:quiz_app/feature/quiz_play/controller/quiz_play_controller.dart'; class QuizResultController extends GetxController { + late final QuizData question; late final List questions; late final List answers; @@ -20,7 +24,8 @@ class QuizResultController extends GetxController { void loadData() { final args = Get.arguments as List; - questions = args[0] as List; + question = args[0] as QuizData; + questions = question.questionListings; answers = args[1] as List; totalQuestions.value = questions.length; } @@ -40,4 +45,49 @@ class QuizResultController extends GetxController { return "Belum Lulus 😔"; } } + + QuestionData mapQuestionListingToQuestionData(QuestionListing questionListing, int index) { + // Convert type string ke enum + QuestionType? questionType; + switch (questionListing.type) { + case 'fill_the_blank': + questionType = QuestionType.fillTheBlank; + break; + case 'option': + questionType = QuestionType.option; + break; + case 'true_false': + questionType = QuestionType.trueOrFalse; + break; + default: + questionType = null; + } + + // Convert options ke OptionData + List? optionDataList; + if (questionListing.options != null) { + optionDataList = []; + for (int i = 0; i < questionListing.options!.length; i++) { + optionDataList.add(OptionData(index: i, text: questionListing.options![i])); + } + } + + // Cari correctAnswerIndex kalau tipe-nya option + int? correctAnswerIndex; + if (questionType == QuestionType.option && optionDataList != null) { + correctAnswerIndex = optionDataList.indexWhere((option) => option.text == questionListing.targetAnswer); + if (correctAnswerIndex == -1) { + correctAnswerIndex = null; // Kalau tidak ketemu + } + } + + return QuestionData( + index: index, + question: questionListing.question, + answer: questionListing.targetAnswer, + options: optionDataList, + correctAnswerIndex: correctAnswerIndex, + type: questionType, + ); + } } diff --git a/lib/feature/quiz_result/view/quiz_result_view.dart b/lib/feature/quiz_result/view/quiz_result_view.dart index 9e226cb..059b742 100644 --- a/lib/feature/quiz_result/view/quiz_result_view.dart +++ b/lib/feature/quiz_result/view/quiz_result_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/widget/question_container_widget.dart'; import 'package:quiz_app/feature/quiz_result/controller/quiz_result_controller.dart'; class QuizResultView extends GetView { @@ -33,7 +34,9 @@ class QuizResultView extends GetView { child: ListView.builder( itemCount: controller.questions.length, itemBuilder: (context, index) { - return _buildQuestionResult(index); + return QuestionContainerWidget( + question: controller.mapQuestionListingToQuestionData(controller.questions[index], index), + answeredQuestion: controller.answers[index]); }, ), ), @@ -71,76 +74,76 @@ class QuizResultView extends GetView { ); } - Widget _buildQuestionResult(int index) { - final answered = controller.answers[index]; - final isCorrect = answered.isCorrect; + // Widget _buildQuestionResult(int index) { + // final answered = controller.answers[index]; + // final isCorrect = answered.isCorrect; - return Container( - width: double.infinity, - margin: const EdgeInsets.only(bottom: 16), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.borderLight), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 6, - offset: const Offset(2, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Soal ${answered.index + 1}', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: AppColors.darkText)), - const SizedBox(height: 6), - Text( - // Tidak ada tipe soal di AnsweredQuestion, jadi kalau mau kasih tipe harus pakai question[index].type - // kalau tidak mau ribet, ini bisa dihapus saja - // controller.mapQuestionTypeToText(controller.questions[answered.index].type), - '', // kosongkan dulu - style: const TextStyle(fontSize: 12, color: AppColors.softGrayText, fontStyle: FontStyle.italic), - ), - const SizedBox(height: 12), - Text( - answered.question, - style: const TextStyle(fontSize: 16, color: AppColors.darkText), - ), - const SizedBox(height: 12), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Icon(Icons.person, size: 18, color: AppColors.primaryBlue), - const SizedBox(width: 6), - Expanded( - child: Text( - "Jawaban Kamu: ${answered.selectedAnswer}", - style: TextStyle( - color: isCorrect ? Colors.green : Colors.red, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - const SizedBox(height: 4), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Icon(Icons.check, size: 18, color: AppColors.softGrayText), - const SizedBox(width: 6), - Expanded( - child: Text( - "Jawaban Benar: ${answered.correctAnswer}", - style: const TextStyle(color: AppColors.darkText), - ), - ), - ], - ), - ], - ), - ); - } + // return Container( + // width: double.infinity, + // margin: const EdgeInsets.only(bottom: 16), + // padding: const EdgeInsets.all(16), + // decoration: BoxDecoration( + // color: Colors.white, + // borderRadius: BorderRadius.circular(12), + // border: Border.all(color: AppColors.borderLight), + // boxShadow: [ + // BoxShadow( + // color: Colors.black.withOpacity(0.05), + // blurRadius: 6, + // offset: const Offset(2, 2), + // ), + // ], + // ), + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Text('Soal ${answered.index + 1}', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: AppColors.darkText)), + // const SizedBox(height: 6), + // Text( + // // Tidak ada tipe soal di AnsweredQuestion, jadi kalau mau kasih tipe harus pakai question[index].type + // // kalau tidak mau ribet, ini bisa dihapus saja + // // controller.mapQuestionTypeToText(controller.questions[answered.index].type), + // '', // kosongkan dulu + // style: const TextStyle(fontSize: 12, color: AppColors.softGrayText, fontStyle: FontStyle.italic), + // ), + // const SizedBox(height: 12), + // Text( + // answered.question, + // style: const TextStyle(fontSize: 16, color: AppColors.darkText), + // ), + // const SizedBox(height: 12), + // Row( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // const Icon(Icons.person, size: 18, color: AppColors.primaryBlue), + // const SizedBox(width: 6), + // Expanded( + // child: Text( + // "Jawaban Kamu: ${answered.selectedAnswer}", + // style: TextStyle( + // color: isCorrect ? Colors.green : Colors.red, + // fontWeight: FontWeight.bold, + // ), + // ), + // ), + // ], + // ), + // const SizedBox(height: 4), + // Row( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // const Icon(Icons.check, size: 18, color: AppColors.softGrayText), + // const SizedBox(width: 6), + // Expanded( + // child: Text( + // "Jawaban Benar: ${answered.correctAnswer}", + // style: const TextStyle(color: AppColors.darkText), + // ), + // ), + // ], + // ), + // ], + // ), + // ); + // } }