From 6bf48df48ab2c3652ea69a7a757d0e4cac7a0dc2 Mon Sep 17 00:00:00 2001 From: akhdanre Date: Sun, 4 May 2025 01:15:56 +0700 Subject: [PATCH] fix: listing request model and logic --- lib/data/models/quiz/library_quiz_model.dart | 18 ++++- .../quiz/question/base_qustion_model.dart | 32 ++++++++ .../fill_in_the_blank_question_model.dart | 31 +++++++ .../quiz/question/option_question_model.dart | 34 ++++++++ .../question/true_false_question_model.dart | 31 +++++++ .../models/quiz/question_listings_model.dart | 2 +- lib/data/services/quiz_service.dart | 25 +++--- .../controller/detail_quiz_controller.dart | 8 +- .../detail_quiz/view/detail_quix_view.dart | 4 +- .../controller/history_controller.dart | 3 + lib/feature/history/view/history_view.dart | 81 ++++++++++--------- .../controller/library_controller.dart | 13 +-- lib/feature/library/view/library_view.dart | 10 +-- .../controller/quiz_play_controller.dart | 37 ++++++--- .../quiz_play/view/quiz_play_view.dart | 74 ++++++++--------- .../controller/quiz_result_controller.dart | 2 +- 16 files changed, 284 insertions(+), 121 deletions(-) create mode 100644 lib/data/models/quiz/question/base_qustion_model.dart create mode 100644 lib/data/models/quiz/question/fill_in_the_blank_question_model.dart create mode 100644 lib/data/models/quiz/question/option_question_model.dart create mode 100644 lib/data/models/quiz/question/true_false_question_model.dart diff --git a/lib/data/models/quiz/library_quiz_model.dart b/lib/data/models/quiz/library_quiz_model.dart index e60214c..cc8d7a8 100644 --- a/lib/data/models/quiz/library_quiz_model.dart +++ b/lib/data/models/quiz/library_quiz_model.dart @@ -1,21 +1,27 @@ -import 'package:quiz_app/data/models/quiz/question_listings_model.dart'; +import 'package:quiz_app/data/models/quiz/question/base_qustion_model.dart'; class QuizData { final String authorId; + final String subjectId; + final String subjectName; final String title; final String? description; final bool isPublic; final String? date; + final String? time; final int totalQuiz; final int limitDuration; - final List questionListings; + final List questionListings; QuizData({ required this.authorId, + required this.subjectId, + required this.subjectName, required this.title, this.description, required this.isPublic, this.date, + this.time, required this.totalQuiz, required this.limitDuration, required this.questionListings, @@ -24,23 +30,29 @@ class QuizData { factory QuizData.fromJson(Map json) { return QuizData( authorId: json['author_id'], + subjectId: json['subject_id'], + subjectName: json['subject_alias'], title: json['title'], description: json['description'], isPublic: json['is_public'], date: json['date'], + time: json['time'], totalQuiz: json['total_quiz'], limitDuration: json['limit_duration'], - questionListings: (json['question_listings'] as List).map((e) => QuestionListing.fromJson(e)).toList(), + questionListings: (json['question_listings'] as List).map((e) => BaseQuestionModel.fromJson(e as Map)).toList(), ); } Map toJson() { return { 'author_id': authorId, + 'subject_id': subjectId, + 'subject_alias': subjectName, 'title': title, 'description': description, 'is_public': isPublic, 'date': date, + 'time': time, 'total_quiz': totalQuiz, 'limit_duration': limitDuration, 'question_listings': questionListings.map((e) => e.toJson()).toList(), diff --git a/lib/data/models/quiz/question/base_qustion_model.dart b/lib/data/models/quiz/question/base_qustion_model.dart new file mode 100644 index 0000000..31843b2 --- /dev/null +++ b/lib/data/models/quiz/question/base_qustion_model.dart @@ -0,0 +1,32 @@ +import 'package:quiz_app/data/models/quiz/question/fill_in_the_blank_question_model.dart'; +import 'package:quiz_app/data/models/quiz/question/option_question_model.dart'; +import 'package:quiz_app/data/models/quiz/question/true_false_question_model.dart'; + +abstract class BaseQuestionModel { + final int index; + final String question; + final int duration; + final String type; + + BaseQuestionModel({ + required this.index, + required this.question, + required this.duration, + required this.type, + }); + + factory BaseQuestionModel.fromJson(Map json) { + switch (json['type']) { + case 'fill_the_blank': + return FillInTheBlankQuestion.fromJson(json); + case 'true_false': + return TrueFalseQuestion.fromJson(json); + case 'option': + return OptionQuestion.fromJson(json); + default: + throw Exception('Unsupported question type: ${json['type']}'); + } + } + + Map toJson(); +} diff --git a/lib/data/models/quiz/question/fill_in_the_blank_question_model.dart b/lib/data/models/quiz/question/fill_in_the_blank_question_model.dart new file mode 100644 index 0000000..63aa35c --- /dev/null +++ b/lib/data/models/quiz/question/fill_in_the_blank_question_model.dart @@ -0,0 +1,31 @@ +import 'package:quiz_app/data/models/quiz/question/base_qustion_model.dart'; + +class FillInTheBlankQuestion extends BaseQuestionModel { + final String targetAnswer; + + FillInTheBlankQuestion({ + required int index, + required String question, + required int duration, + required this.targetAnswer, + }) : super(index: index, question: question, duration: duration, type: 'fill_the_blank'); + + factory FillInTheBlankQuestion.fromJson(Map json) { + return FillInTheBlankQuestion( + index: json['index'], + question: json['question'], + duration: json['duration'], + targetAnswer: json['target_answer'], + ); + } + + @override + Map toJson() => { + 'index': index, + 'question': question, + 'duration': duration, + 'type': type, + 'target_answer': targetAnswer, + 'options': null, + }; +} diff --git a/lib/data/models/quiz/question/option_question_model.dart b/lib/data/models/quiz/question/option_question_model.dart new file mode 100644 index 0000000..e04355a --- /dev/null +++ b/lib/data/models/quiz/question/option_question_model.dart @@ -0,0 +1,34 @@ +import 'package:quiz_app/data/models/quiz/question/base_qustion_model.dart'; + +class OptionQuestion extends BaseQuestionModel { + final int targetAnswer; + final List options; + + OptionQuestion({ + required int index, + required String question, + required int duration, + required this.targetAnswer, + required this.options, + }) : super(index: index, question: question, duration: duration, type: 'option'); + + factory OptionQuestion.fromJson(Map json) { + return OptionQuestion( + index: json['index'], + question: json['question'], + duration: json['duration'], + targetAnswer: json['target_answer'], + options: List.from(json['options']), + ); + } + + @override + Map toJson() => { + 'index': index, + 'question': question, + 'duration': duration, + 'type': type, + 'target_answer': targetAnswer, + 'options': options, + }; +} diff --git a/lib/data/models/quiz/question/true_false_question_model.dart b/lib/data/models/quiz/question/true_false_question_model.dart new file mode 100644 index 0000000..e4f0768 --- /dev/null +++ b/lib/data/models/quiz/question/true_false_question_model.dart @@ -0,0 +1,31 @@ +import 'package:quiz_app/data/models/quiz/question/base_qustion_model.dart'; + +class TrueFalseQuestion extends BaseQuestionModel { + final bool targetAnswer; + + TrueFalseQuestion({ + required int index, + required String question, + required int duration, + required this.targetAnswer, + }) : super(index: index, question: question, duration: duration, type: 'true_false'); + + factory TrueFalseQuestion.fromJson(Map json) { + return TrueFalseQuestion( + index: json['index'], + question: json['question'], + duration: json['duration'], + targetAnswer: json['target_answer'], + ); + } + + @override + Map toJson() => { + 'index': index, + 'question': question, + 'duration': duration, + 'type': type, + 'target_answer': targetAnswer, + 'options': null, + }; +} diff --git a/lib/data/models/quiz/question_listings_model.dart b/lib/data/models/quiz/question_listings_model.dart index 2e74dde..32ab571 100644 --- a/lib/data/models/quiz/question_listings_model.dart +++ b/lib/data/models/quiz/question_listings_model.dart @@ -1,7 +1,7 @@ class QuestionListing { final int index; final String question; - final String targetAnswer; + final dynamic targetAnswer; final int duration; final String type; final List? options; diff --git a/lib/data/services/quiz_service.dart b/lib/data/services/quiz_service.dart index d41392b..66e590e 100644 --- a/lib/data/services/quiz_service.dart +++ b/lib/data/services/quiz_service.dart @@ -35,19 +35,22 @@ class QuizService extends GetxService { } } - Future> userQuiz(String userId, int page) async { + Future>?> userQuiz(String userId, int page) async { try { final response = await _dio.get("${APIEndpoint.userQuiz}/$userId?page=$page"); - - final parsedResponse = BaseResponseModel>.fromJson( - response.data, - (data) => (data as List).map((e) => QuizData.fromJson(e as Map)).toList(), - ); - - return parsedResponse.data ?? []; + if (response.statusCode == 200) { + final parsedResponse = BaseResponseModel>.fromJson( + response.data, + (data) => (data as List).map((e) => QuizListingModel.fromJson(e as Map)).toList(), + ); + return parsedResponse; + } else { + logC.e("Failed to fetch recommendation quizzes. Status: ${response.statusCode}"); + return null; + } } catch (e) { logC.e("Error fetching user quizzes: $e"); - return []; + return null; } } @@ -105,8 +108,8 @@ class QuizService extends GetxService { logC.e("Failed to fetch quiz by id. Status: ${response.statusCode}"); return null; } - } catch (e) { - logC.e("Error fetching quiz by id $e"); + } catch (e, stacktrace) { + logC.e("Error fetching quiz by id $e", stackTrace: stacktrace); return null; } } diff --git a/lib/feature/detail_quiz/controller/detail_quiz_controller.dart b/lib/feature/detail_quiz/controller/detail_quiz_controller.dart index a4ecddf..e089de2 100644 --- a/lib/feature/detail_quiz/controller/detail_quiz_controller.dart +++ b/lib/feature/detail_quiz/controller/detail_quiz_controller.dart @@ -20,12 +20,8 @@ class DetailQuizController extends GetxController { } void loadData() async { - final args = Get.arguments; - if (args is QuizData) { - data = args; - } else { - getQuizData(args); - } + final quizId = Get.arguments as String; + getQuizData(quizId); } void getQuizData(String quizId) async { diff --git a/lib/feature/detail_quiz/view/detail_quix_view.dart b/lib/feature/detail_quiz/view/detail_quix_view.dart index a178585..6836916 100644 --- a/lib/feature/detail_quiz/view/detail_quix_view.dart +++ b/lib/feature/detail_quiz/view/detail_quix_view.dart @@ -3,7 +3,7 @@ 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/component/widget/loading_widget.dart'; -import 'package:quiz_app/data/models/quiz/question_listings_model.dart'; +import 'package:quiz_app/data/models/quiz/question/base_qustion_model.dart'; import 'package:quiz_app/feature/detail_quiz/controller/detail_quiz_controller.dart'; class DetailQuizView extends GetView { @@ -100,7 +100,7 @@ class DetailQuizView extends GetView { ); } - Widget _buildQuestionItem(QuestionListing question, int index) { + Widget _buildQuestionItem(BaseQuestionModel question, int index) { return Container( width: double.infinity, margin: const EdgeInsets.only(bottom: 16), diff --git a/lib/feature/history/controller/history_controller.dart b/lib/feature/history/controller/history_controller.dart index 44a2f5d..786e262 100644 --- a/lib/feature/history/controller/history_controller.dart +++ b/lib/feature/history/controller/history_controller.dart @@ -9,6 +9,8 @@ class HistoryController extends GetxController { HistoryController(this._historyService, this._userController); + RxBool isLoading = true.obs; + final historyList = [].obs; @override @@ -19,5 +21,6 @@ class HistoryController extends GetxController { void loadDummyHistory() async { historyList.value = await _historyService.getHistory(_userController.userData!.id) ?? []; + isLoading.value = false; } } diff --git a/lib/feature/history/view/history_view.dart b/lib/feature/history/view/history_view.dart index 2b9a09e..e8ab89f 100644 --- a/lib/feature/history/view/history_view.dart +++ b/lib/feature/history/view/history_view.dart @@ -13,45 +13,54 @@ class HistoryView extends GetView { body: SafeArea( child: Padding( padding: const EdgeInsets.all(16), - child: Obx(() { - final historyList = controller.historyList; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - "Riwayat Kuis", - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Riwayat Kuis", + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, ), - const SizedBox(height: 8), - const Text( - "Lihat kembali hasil kuis yang telah kamu kerjakan", - style: TextStyle( - fontSize: 14, - color: Colors.grey, - ), + ), + const SizedBox(height: 8), + const Text( + "Lihat kembali hasil kuis yang telah kamu kerjakan", + style: TextStyle( + fontSize: 14, + color: Colors.grey, ), - const SizedBox(height: 20), - if (historyList.isEmpty) - const Expanded( - child: Center(child: LoadingWidget()), - ) - else - Expanded( - child: ListView.builder( - itemCount: historyList.length, - itemBuilder: (context, index) { - final item = historyList[index]; - return _buildHistoryCard(item); - }, + ), + const SizedBox(height: 20), + Obx(() { + if (controller.isLoading.value) { + return Expanded( + child: Center( + child: LoadingWidget(), ), - ) - ], - ); - }), + ); + } + + final historyList = controller.historyList; + + if (historyList.isEmpty) { + return const Expanded( + child: Center(child: Text("you still doesnt have quiz history")), + ); + } + + return Expanded( + child: ListView.builder( + itemCount: historyList.length, + itemBuilder: (context, index) { + final item = historyList[index]; + return _buildHistoryCard(item); + }, + ), + ); + }), + ], + ), ), ), ); diff --git a/lib/feature/library/controller/library_controller.dart b/lib/feature/library/controller/library_controller.dart index 93d0da5..37a5425 100644 --- a/lib/feature/library/controller/library_controller.dart +++ b/lib/feature/library/controller/library_controller.dart @@ -1,11 +1,12 @@ import 'package:get/get.dart'; import 'package:quiz_app/app/routes/app_pages.dart'; import 'package:quiz_app/data/controllers/user_controller.dart'; -import 'package:quiz_app/data/models/quiz/library_quiz_model.dart'; +import 'package:quiz_app/data/models/base/base_model.dart'; +import 'package:quiz_app/data/models/quiz/quiz_listing_model.dart'; import 'package:quiz_app/data/services/quiz_service.dart'; class LibraryController extends GetxController { - RxList quizs = [].obs; + RxList quizs = [].obs; RxBool isLoading = true.obs; RxString emptyMessage = "".obs; @@ -24,11 +25,11 @@ class LibraryController extends GetxController { void loadUserQuiz() async { try { isLoading.value = true; - List data = await _quizService.userQuiz(_userController.userData!.id, currentPage); - if (data.isEmpty) { + BaseResponseModel>? response = await _quizService.userQuiz(_userController.userData!.id, currentPage); + if (response == null) { emptyMessage.value = "Kamu belum membuat soal."; } else { - quizs.addAll(data); + quizs.assignAll(response.data!); } } catch (e) { emptyMessage.value = "Terjadi kesalahan saat memuat data."; @@ -38,7 +39,7 @@ class LibraryController extends GetxController { } void goToDetail(int index) { - Get.toNamed(AppRoutes.detailQuizPage, arguments: quizs[index]); + Get.toNamed(AppRoutes.detailQuizPage, arguments: quizs[index].quizId); } String formatDuration(int seconds) { diff --git a/lib/feature/library/view/library_view.dart b/lib/feature/library/view/library_view.dart index 58bcbcb..916173f 100644 --- a/lib/feature/library/view/library_view.dart +++ b/lib/feature/library/view/library_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:quiz_app/component/widget/loading_widget.dart'; -import 'package:quiz_app/data/models/quiz/library_quiz_model.dart'; +import 'package:quiz_app/data/models/quiz/quiz_listing_model.dart'; import 'package:quiz_app/feature/library/controller/library_controller.dart'; class LibraryView extends GetView { @@ -65,7 +65,7 @@ class LibraryView extends GetView { ); } - Widget _buildQuizCard(QuizData quiz) { + Widget _buildQuizCard(QuizListingModel quiz) { return Container( margin: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.all(16), @@ -108,7 +108,7 @@ class LibraryView extends GetView { ), const SizedBox(height: 4), Text( - quiz.description ?? "", + quiz.description, style: const TextStyle( color: Colors.grey, fontSize: 12, @@ -122,7 +122,7 @@ class LibraryView extends GetView { const Icon(Icons.calendar_today_rounded, size: 14, color: Colors.grey), const SizedBox(width: 4), Text( - controller.formatDate(quiz.date ?? ""), + controller.formatDate(quiz.date), style: const TextStyle(fontSize: 12, color: Colors.grey), ), const SizedBox(width: 12), @@ -136,7 +136,7 @@ class LibraryView extends GetView { const Icon(Icons.access_time, size: 14, color: Colors.grey), const SizedBox(width: 4), Text( - controller.formatDuration(quiz.limitDuration), + controller.formatDuration(quiz.duration), style: const TextStyle(fontSize: 12, color: Colors.grey), ), ], diff --git a/lib/feature/quiz_play/controller/quiz_play_controller.dart b/lib/feature/quiz_play/controller/quiz_play_controller.dart index 9e28bf6..ed5bd01 100644 --- a/lib/feature/quiz_play/controller/quiz_play_controller.dart +++ b/lib/feature/quiz_play/controller/quiz_play_controller.dart @@ -5,7 +5,7 @@ import 'package:get/get.dart'; import 'package:quiz_app/app/routes/app_pages.dart'; import 'package:quiz_app/component/notification/pop_up_confirmation.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/question/base_qustion_model.dart'; class QuizPlayController extends GetxController { late final QuizData quizData; @@ -28,7 +28,7 @@ class QuizPlayController extends GetxController { Timer? _timer; - QuestionListing get currentQuestion => quizData.questionListings[currentIndex.value]; + BaseQuestionModel get currentQuestion => quizData.questionListings[currentIndex.value]; @override void onInit() { @@ -99,14 +99,14 @@ class QuizPlayController extends GetxController { userAnswer = selectedAnswer.value.trim(); break; } - answeredQuestions.add(AnsweredQuestion( - index: currentIndex.value, - questionIndex: question.index, - selectedAnswer: userAnswer, - correctAnswer: question.targetAnswer.trim(), - isCorrect: userAnswer.toLowerCase() == question.targetAnswer.trim().toLowerCase(), - duration: currentQuestion.duration - timeLeft.value, - )); + // answeredQuestions.add(AnsweredQuestion( + // index: currentIndex.value, + // questionIndex: question.index, + // selectedAnswer: userAnswer, + // correctAnswer: question., + // isCorrect: userAnswer.toLowerCase() == question.targetAnswer.trim().toLowerCase(), + // duration: currentQuestion.duration - timeLeft.value, + // )); } void nextQuestion() { @@ -136,7 +136,7 @@ class QuizPlayController extends GetxController { void _finishQuiz() async { _timer?.cancel(); - + AppDialog.showMessage(Get.context!, "Yeay semua soal selesai"); await Future.delayed(Duration(seconds: 2)); Get.offAllNamed( @@ -156,8 +156,8 @@ class QuizPlayController extends GetxController { class AnsweredQuestion { final int index; final int questionIndex; - final String selectedAnswer; - final String correctAnswer; + final dynamic selectedAnswer; + final dynamic correctAnswer; final bool isCorrect; final int duration; @@ -170,6 +170,17 @@ class AnsweredQuestion { required this.duration, }); + factory AnsweredQuestion.fromJson(Map json) { + return AnsweredQuestion( + index: json['index'], + questionIndex: json['question_index'], + selectedAnswer: json['selectedAnswer'], + correctAnswer: json['correctAnswer'], + isCorrect: json['isCorrect'], + duration: json['duration'], + ); + } + Map toJson() => { 'index': index, 'question_index': questionIndex, diff --git a/lib/feature/quiz_play/view/quiz_play_view.dart b/lib/feature/quiz_play/view/quiz_play_view.dart index dc6ac95..e06e727 100644 --- a/lib/feature/quiz_play/view/quiz_play_view.dart +++ b/lib/feature/quiz_play/view/quiz_play_view.dart @@ -35,7 +35,7 @@ class QuizPlayView extends GetView { const SizedBox(height: 12), _buildQuestionText(), const SizedBox(height: 30), - _buildAnswerSection(), + // _buildAnswerSection(), const Spacer(), _buildNextButton(), ], @@ -100,44 +100,44 @@ class QuizPlayView extends GetView { ); } - Widget _buildAnswerSection() { - final question = controller.currentQuestion; + // Widget _buildAnswerSection() { + // final question = controller.currentQuestion; - if (question.type == 'option' && question.options != null) { - return Column( - children: List.generate(question.options!.length, (index) { - final option = question.options![index]; - final isSelected = controller.idxOptionSelected.value == index; + // if (question.type == 'option' && question.options != null) { + // return Column( + // children: List.generate(question.options!.length, (index) { + // final option = question.options![index]; + // final isSelected = controller.idxOptionSelected.value == index; - return Container( - margin: const EdgeInsets.only(bottom: 12), - width: double.infinity, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: isSelected ? AppColors.primaryBlue : Colors.white, - foregroundColor: isSelected ? Colors.white : Colors.black, - side: const BorderSide(color: Colors.grey), - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - ), - onPressed: () => controller.selectAnswerOption(index), - child: Text(option), - ), - ); - }), - ); - } else if (question.type == 'true_false') { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _buildTrueFalseButton('Ya', true, controller.choosenAnswerTOF), - _buildTrueFalseButton('Tidak', false, controller.choosenAnswerTOF), - ], - ); - } else { - return GlobalTextField(controller: controller.answerTextController); - } - } + // return Container( + // margin: const EdgeInsets.only(bottom: 12), + // width: double.infinity, + // child: ElevatedButton( + // style: ElevatedButton.styleFrom( + // backgroundColor: isSelected ? AppColors.primaryBlue : Colors.white, + // foregroundColor: isSelected ? Colors.white : Colors.black, + // side: const BorderSide(color: Colors.grey), + // padding: const EdgeInsets.symmetric(vertical: 14), + // shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + // ), + // onPressed: () => controller.selectAnswerOption(index), + // child: Text(option), + // ), + // ); + // }), + // ); + // } else if (question.type == 'true_false') { + // return Row( + // mainAxisAlignment: MainAxisAlignment.spaceEvenly, + // children: [ + // _buildTrueFalseButton('Ya', true, controller.choosenAnswerTOF), + // _buildTrueFalseButton('Tidak', false, controller.choosenAnswerTOF), + // ], + // ); + // } else { + // return GlobalTextField(controller: controller.answerTextController); + // } + // } Widget _buildTrueFalseButton(String label, bool value, RxInt choosenAnswer) { return Obx(() { diff --git a/lib/feature/quiz_result/controller/quiz_result_controller.dart b/lib/feature/quiz_result/controller/quiz_result_controller.dart index ee36b26..5bf445a 100644 --- a/lib/feature/quiz_result/controller/quiz_result_controller.dart +++ b/lib/feature/quiz_result/controller/quiz_result_controller.dart @@ -26,7 +26,7 @@ class QuizResultController extends GetxController { final args = Get.arguments as List; question = args[0] as QuizData; - questions = question.questionListings; + // questions = question.questionListings; answers = args[1] as List; totalQuestions.value = questions.length; }