From 92f349e8badf2d63147a9b539fce0b921a5e355f Mon Sep 17 00:00:00 2001 From: akhdanre Date: Tue, 29 Apr 2025 23:00:48 +0700 Subject: [PATCH] feat: add index and input validation --- .../widget/question_container_widget.dart | 4 +- .../models/quiz/question_listings_model.dart | 4 + .../models/quiz/quiestion_data_model.dart | 12 +- .../controller/quiz_creation_controller.dart | 61 +++++-- .../component/custom_question_component.dart | 28 ++-- .../controller/quiz_play_controller.dart | 13 +- .../controller/quiz_preview_controller.dart | 9 +- .../controller/quiz_result_controller.dart | 11 +- .../quiz_result/view/quiz_result_view.dart | 149 ++++++------------ 9 files changed, 130 insertions(+), 161 deletions(-) diff --git a/lib/component/widget/question_container_widget.dart b/lib/component/widget/question_container_widget.dart index 9cd75e4..18b71d4 100644 --- a/lib/component/widget/question_container_widget.dart +++ b/lib/component/widget/question_container_widget.dart @@ -49,8 +49,8 @@ class QuestionContainerWidget extends StatelessWidget { _buildAnsweredSection(answeredQuestion!), ], const SizedBox(height: 10), - const Text( - 'Durasi: 0 detik', + Text( + 'Durasi: ${question.duration} detik', style: TextStyle(fontSize: 14, color: AppColors.softGrayText), ), ], diff --git a/lib/data/models/quiz/question_listings_model.dart b/lib/data/models/quiz/question_listings_model.dart index 0c5a331..2e74dde 100644 --- a/lib/data/models/quiz/question_listings_model.dart +++ b/lib/data/models/quiz/question_listings_model.dart @@ -1,4 +1,5 @@ class QuestionListing { + final int index; final String question; final String targetAnswer; final int duration; @@ -6,6 +7,7 @@ class QuestionListing { final List? options; QuestionListing({ + required this.index, required this.question, required this.targetAnswer, required this.duration, @@ -15,6 +17,7 @@ class QuestionListing { factory QuestionListing.fromJson(Map json) { return QuestionListing( + index: json['index'], question: json['question'], targetAnswer: json['target_answer'], duration: json['duration'], @@ -25,6 +28,7 @@ class QuestionListing { Map toJson() { return { + 'index': index, 'question': question, 'target_answer': targetAnswer, 'duration': duration, diff --git a/lib/data/models/quiz/quiestion_data_model.dart b/lib/data/models/quiz/quiestion_data_model.dart index 10a7880..55d06a7 100644 --- a/lib/data/models/quiz/quiestion_data_model.dart +++ b/lib/data/models/quiz/quiestion_data_model.dart @@ -14,6 +14,7 @@ class QuestionData { final List? options; final int? correctAnswerIndex; final QuestionType? type; + final int duration; QuestionData({ required this.index, @@ -21,17 +22,11 @@ class QuestionData { this.answer, this.options, this.correctAnswerIndex, + this.duration = 30, this.type, }); - QuestionData copyWith({ - int? index, - String? question, - String? answer, - List? options, - int? correctAnswerIndex, - QuestionType? type, - }) { + QuestionData copyWith({int? index, String? question, String? answer, List? options, int? correctAnswerIndex, QuestionType? type, int? duration}) { return QuestionData( index: index ?? this.index, question: question ?? this.question, @@ -39,6 +34,7 @@ class QuestionData { options: options ?? this.options, correctAnswerIndex: correctAnswerIndex ?? this.correctAnswerIndex, type: type ?? this.type, + duration: duration ?? this.duration, ); } } diff --git a/lib/feature/quiz_creation/controller/quiz_creation_controller.dart b/lib/feature/quiz_creation/controller/quiz_creation_controller.dart index 234ac02..5d72a76 100644 --- a/lib/feature/quiz_creation/controller/quiz_creation_controller.dart +++ b/lib/feature/quiz_creation/controller/quiz_creation_controller.dart @@ -18,6 +18,8 @@ class QuizCreationController extends GetxController { RxList quizData = [QuestionData(index: 1, type: QuestionType.fillTheBlank)].obs; RxInt selectedQuizIndex = 0.obs; + RxInt currentDuration = 30.obs; + @override void onInit() { super.onInit(); @@ -91,6 +93,8 @@ class QuizCreationController extends GetxController { questionTC.text = data.question ?? ""; answerTC.text = data.answer ?? ""; + currentDuration.value = data.duration; + currentQuestionType.value = data.type ?? QuestionType.fillTheBlank; if (currentQuestionType.value == QuestionType.option) { for (int i = 0; i < optionTCList.length; i++) { @@ -118,13 +122,7 @@ class QuizCreationController extends GetxController { } } - void _updateCurrentQuestion({ - String? question, - String? answer, - List? options, - int? correctAnswerIndex, - QuestionType? type, - }) { + void _updateCurrentQuestion({String? question, String? answer, List? options, int? correctAnswerIndex, QuestionType? type, int? duration}) { final current = quizData[selectedQuizIndex.value]; quizData[selectedQuizIndex.value] = current.copyWith( question: question, @@ -132,22 +130,55 @@ class QuizCreationController extends GetxController { options: options, correctAnswerIndex: correctAnswerIndex, type: type, + duration: duration, ); - - // ?? - // (currentQuestionType.value == QuestionType.option - // ? List.generate( - // optionTCList.length, - // (index) => OptionData(index: index, text: optionTCList[index].text), - // ) - // : null), } void updateTOFAnswer(bool answer) { _updateCurrentQuestion(answer: answer.toString()); } + void onDurationChange(int? duration) { + currentDuration.value = duration ?? 30; + _updateCurrentQuestion(duration: duration); + } + void onDone() { + for (final value in quizData) { + if (value.question == null) { + Get.snackbar( + 'Field kosong di soal ${value.index}', + 'Hapus jika tidak digunakan', + ); + return; + } + + if (value.type == QuestionType.option) { + if (value.correctAnswerIndex == null) { + Get.snackbar( + 'Field kosong di soal ${value.index}', + 'Hapus jika tidak digunakan', + ); + return; + } + if (value.options == null || value.options!.length < 4) { + Get.snackbar( + 'Pilihan jawaban kurang dari 4 di soal ${value.index}', + 'Tambahkan pilihan jawaban', + ); + return; + } + } else { + if (value.answer == null) { + Get.snackbar( + 'Field kosong di soal ${value.index}', + 'Hapus jika tidak digunakan', + ); + return; + } + } + } + Get.toNamed(AppRoutes.quizPreviewPage, arguments: quizData); } 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 18b0ee2..c543240 100644 --- a/lib/feature/quiz_creation/view/component/custom_question_component.dart +++ b/lib/feature/quiz_creation/view/component/custom_question_component.dart @@ -180,20 +180,20 @@ class CustomQuestionComponent extends GetView { borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.borderLight), ), - child: DropdownButtonFormField( - value: '1 minute', - decoration: const InputDecoration( - border: InputBorder.none, - contentPadding: EdgeInsets.symmetric(vertical: 14), - ), - style: const TextStyle(color: AppColors.darkText, fontWeight: FontWeight.w500), - items: const [ - DropdownMenuItem(value: '30 seconds', child: Text('30 seconds')), - DropdownMenuItem(value: '1 minute', child: Text('1 minute')), - DropdownMenuItem(value: '2 minutes', child: Text('2 minutes')), - ], - onChanged: (value) {}, - ), + child: Obx(() => DropdownButtonFormField( + value: controller.currentDuration.value, + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 14), + ), + style: const TextStyle(color: AppColors.darkText, fontWeight: FontWeight.w500), + items: const [ + DropdownMenuItem(value: 10, child: Text('10 detik')), + DropdownMenuItem(value: 20, child: Text('20 detik')), + DropdownMenuItem(value: 30, child: Text('30 detik')), + DropdownMenuItem(value: 60, child: Text('1 menit')), + ], + onChanged: controller.onDurationChange)), ); } } diff --git a/lib/feature/quiz_play/controller/quiz_play_controller.dart b/lib/feature/quiz_play/controller/quiz_play_controller.dart index ce1a4cd..355fa0a 100644 --- a/lib/feature/quiz_play/controller/quiz_play_controller.dart +++ b/lib/feature/quiz_play/controller/quiz_play_controller.dart @@ -123,17 +123,8 @@ class QuizPlayController extends GetxController { void _finishQuiz() { _timer?.cancel(); - // logC.i(answeredQuestions.map((e) => e.toJson()).toList()); - // Get.defaultDialog( - // title: "Selesai", - // middleText: "Kamu telah menyelesaikan semua soal!", - // onConfirm: () { - // Get.back(); - // Get.back(); - // }, - // textConfirm: "OK", - // ); - Get.toNamed( + + Get.offAllNamed( AppRoutes.resultQuizPage, 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 0bfa96c..03d87d4 100644 --- a/lib/feature/quiz_preview/controller/quiz_preview_controller.dart +++ b/lib/feature/quiz_preview/controller/quiz_preview_controller.dart @@ -72,10 +72,14 @@ class QuizPreviewController extends GetxController { } List _mapQuestionsToListings(List questions) { - return questions.map((q) { + return questions.asMap().entries.map((entry) { + final index = entry.key; + final q = entry.value; + String typeString; String answer = ""; List? option; + switch (q.type) { case QuestionType.fillTheBlank: typeString = 'fill_the_blank'; @@ -96,9 +100,10 @@ class QuizPreviewController extends GetxController { } return QuestionListing( + index: index, question: q.question ?? '', targetAnswer: answer, - duration: 30, + duration: q.duration, type: typeString, options: option, ); diff --git a/lib/feature/quiz_result/controller/quiz_result_controller.dart b/lib/feature/quiz_result/controller/quiz_result_controller.dart index c6c7884..ee36b26 100644 --- a/lib/feature/quiz_result/controller/quiz_result_controller.dart +++ b/lib/feature/quiz_result/controller/quiz_result_controller.dart @@ -1,5 +1,6 @@ import 'package:get/get.dart'; import 'package:quiz_app/app/const/enums/question_type.dart'; +import 'package:quiz_app/app/routes/app_pages.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'; @@ -39,11 +40,7 @@ class QuizResultController extends GetxController { } String getResultMessage() { - if (scorePercentage.value >= 80) { - return "Lulus 🎉"; - } else { - return "Belum Lulus 😔"; - } + return "Nilai kamu ${scorePercentage.value}"; } QuestionData mapQuestionListingToQuestionData(QuestionListing questionListing, int index) { @@ -90,4 +87,8 @@ class QuizResultController extends GetxController { type: questionType, ); } + + void onPopInvoke(bool isPop, dynamic value) { + Get.offNamed(AppRoutes.mainPage, arguments: 3); + } } diff --git a/lib/feature/quiz_result/view/quiz_result_view.dart b/lib/feature/quiz_result/view/quiz_result_view.dart index 059b742..e15f974 100644 --- a/lib/feature/quiz_result/view/quiz_result_view.dart +++ b/lib/feature/quiz_result/view/quiz_result_view.dart @@ -9,44 +9,58 @@ class QuizResultView extends GetView { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFF9FAFB), - appBar: AppBar( - backgroundColor: Colors.transparent, - elevation: 0, - automaticallyImplyLeading: false, - title: const Text( - 'Hasil Kuis', - style: TextStyle(color: Colors.black, fontWeight: FontWeight.bold), - ), - iconTheme: const IconThemeData(color: Colors.black), - centerTitle: true, - ), - body: SafeArea( - child: Padding( - padding: const EdgeInsets.all(16), - child: Obx(() => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildScoreSummary(), - const SizedBox(height: 16), - Expanded( - child: ListView.builder( - itemCount: controller.questions.length, - itemBuilder: (context, index) { - return QuestionContainerWidget( + return PopScope( + canPop: false, + onPopInvokedWithResult: controller.onPopInvoke, + child: Scaffold( + backgroundColor: const Color(0xFFF9FAFB), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: Obx(() => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildCustomAppBar(context), + const SizedBox(height: 16), + _buildScoreSummary(), + const SizedBox(height: 16), + Expanded( + child: ListView.builder( + itemCount: controller.questions.length, + itemBuilder: (context, index) { + return QuestionContainerWidget( question: controller.mapQuestionListingToQuestionData(controller.questions[index], index), - answeredQuestion: controller.answers[index]); - }, + answeredQuestion: controller.answers[index], + ); + }, + ), ), - ), - ], - )), + ], + )), + ), ), ), ); } + Widget _buildCustomAppBar(BuildContext context) { + return Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.black), + onPressed: () { + controller.onPopInvoke(true, null); + }, + ), + const SizedBox(width: 8), + const Text( + 'Hasil Kuis', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black), + ), + ], + ); + } + Widget _buildScoreSummary() { return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -73,77 +87,4 @@ class QuizResultView extends GetView { ], ); } - - // 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), - // ), - // ), - // ], - // ), - // ], - // ), - // ); - // } }