diff --git a/lib/app/routes/app_pages.dart b/lib/app/routes/app_pages.dart index 41296f7..c791205 100644 --- a/lib/app/routes/app_pages.dart +++ b/lib/app/routes/app_pages.dart @@ -13,6 +13,8 @@ import 'package:quiz_app/feature/navigation/views/navbar_view.dart'; import 'package:quiz_app/feature/profile/binding/profile_binding.dart'; import 'package:quiz_app/feature/quiz_creation/binding/quiz_creation_binding.dart'; import 'package:quiz_app/feature/quiz_creation/view/quiz_creation_view.dart'; +import 'package:quiz_app/feature/quiz_play/binding/quiz_play_binding.dart'; +import 'package:quiz_app/feature/quiz_play/view/quiz_play_view.dart'; import 'package:quiz_app/feature/quiz_preview/binding/quiz_preview_binding.dart'; import 'package:quiz_app/feature/quiz_preview/view/quiz_preview.dart'; import 'package:quiz_app/feature/register/binding/register_binding.dart'; @@ -71,6 +73,11 @@ class AppPages { name: AppRoutes.detailQuizPage, page: () => DetailQuizView(), binding: DetailQuizBinding(), + ), + GetPage( + name: AppRoutes.playQuizPage, + page: () => QuizPlayView(), + binding: QuizPlayBinding(), ) ]; } diff --git a/lib/app/routes/app_routes.dart b/lib/app/routes/app_routes.dart index 8f896b1..6f23a36 100644 --- a/lib/app/routes/app_routes.dart +++ b/lib/app/routes/app_routes.dart @@ -12,4 +12,6 @@ abstract class AppRoutes { static const quizPreviewPage = "/quiz/preview"; static const detailQuizPage = "/quiz/detail"; + + static const playQuizPage = "/quiz/play"; } diff --git a/lib/feature/library/controller/detail_quiz_controller.dart b/lib/feature/library/controller/detail_quiz_controller.dart index 34db30c..e5b04fe 100644 --- a/lib/feature/library/controller/detail_quiz_controller.dart +++ b/lib/feature/library/controller/detail_quiz_controller.dart @@ -1,4 +1,5 @@ import 'package:get/get.dart'; +import 'package:quiz_app/app/routes/app_pages.dart'; import 'package:quiz_app/data/models/quiz/library_quiz_model.dart'; class DetailQuizController extends GetxController { @@ -12,4 +13,6 @@ class DetailQuizController extends GetxController { void loadData() { data = Get.arguments as QuizData; } + + void goToPlayPage() => Get.toNamed(AppRoutes.playQuizPage, arguments: data); } diff --git a/lib/feature/library/view/detail_quix_view.dart b/lib/feature/library/view/detail_quix_view.dart index 36055a5..5b0be23 100644 --- a/lib/feature/library/view/detail_quix_view.dart +++ b/lib/feature/library/view/detail_quix_view.dart @@ -70,7 +70,7 @@ class DetailQuizView extends GetView { const SizedBox(height: 20), const SizedBox(height: 20), - GlobalButton(text: "Kerjakan", onPressed: () {}), + GlobalButton(text: "Kerjakan", onPressed: controller.goToPlayPage), const SizedBox(height: 20), GlobalButton(text: "buat ruangan", onPressed: () {}), diff --git a/lib/feature/quiz_play/binding/quiz_play_binding.dart b/lib/feature/quiz_play/binding/quiz_play_binding.dart new file mode 100644 index 0000000..7b3017e --- /dev/null +++ b/lib/feature/quiz_play/binding/quiz_play_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import 'package:quiz_app/feature/quiz_play/controller/quiz_play_controller.dart'; + +class QuizPlayBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => QuizPlayController()); + } +} diff --git a/lib/feature/quiz_play/controller/quiz_play_controller.dart b/lib/feature/quiz_play/controller/quiz_play_controller.dart new file mode 100644 index 0000000..4736b29 --- /dev/null +++ b/lib/feature/quiz_play/controller/quiz_play_controller.dart @@ -0,0 +1,164 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:get/get.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'; + +class QuizPlayController extends GetxController { + late final QuizData quizData; + + final currentIndex = 0.obs; + final timeLeft = 0.obs; + final isStarting = true.obs; + final isAnswerSelected = false.obs; + final selectedAnswer = ''.obs; + final List answeredQuestions = []; + + final answerTextController = TextEditingController(); + final choosenAnswerTOF = 0.obs; + Timer? _timer; + + QuestionListing get currentQuestion => quizData.questionListings[currentIndex.value]; + + @override + void onInit() { + super.onInit(); + quizData = Get.arguments as QuizData; + _startCountdown(); + + // Listener untuk fill the blank + answerTextController.addListener(() { + if (answerTextController.text.trim().isNotEmpty) { + isAnswerSelected.value = true; + } else { + isAnswerSelected.value = false; + } + }); + + // Listener untuk pilihan true/false + ever(choosenAnswerTOF, (value) { + if (value != 0) { + isAnswerSelected.value = true; + } + }); + } + + void _startCountdown() async { + await Future.delayed(const Duration(seconds: 3)); + isStarting.value = false; + _startTimer(); + } + + void _startTimer() { + timeLeft.value = currentQuestion.duration; + _timer?.cancel(); + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (timeLeft.value > 0) { + timeLeft.value--; + } else { + _submitAnswerIfNeeded(); + _nextQuestion(); + } + }); + isAnswerSelected.value = false; + } + + void selectAnswer(String answer) { + selectedAnswer.value = answer; + isAnswerSelected.value = true; + } + + void onChooseTOF(bool value) { + choosenAnswerTOF.value = value ? 1 : 2; + selectedAnswer.value = value ? "Ya" : "Tidak"; + } + + void _submitAnswerIfNeeded() { + final question = currentQuestion; + String userAnswer = ""; + + if (question.type == "fill_the_blank") { + userAnswer = answerTextController.text.trim(); + } else if (question.type == "option") { + userAnswer = selectedAnswer.value.trim(); + } else if (question.type == "true_false") { + userAnswer = selectedAnswer.value.trim(); + } + + // Masukkan ke answeredQuestions + answeredQuestions.add(AnsweredQuestion( + index: currentIndex.value, + question: question.question, + selectedAnswer: userAnswer, + correctAnswer: question.targetAnswer.trim(), + isCorrect: userAnswer.toLowerCase() == question.targetAnswer.trim().toLowerCase(), + )); + } + + void nextQuestion() { + _submitAnswerIfNeeded(); + _nextQuestion(); + } + + void _nextQuestion() { + _timer?.cancel(); + if (currentIndex.value < quizData.questionListings.length - 1) { + currentIndex.value++; + answerTextController.clear(); + selectedAnswer.value = ''; + choosenAnswerTOF.value = 0; + _startTimer(); + } else { + _finishQuiz(); + } + } + + 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", + ); + } + + @override + void onClose() { + _timer?.cancel(); + answerTextController.dispose(); + super.onClose(); + } +} + +class AnsweredQuestion { + final int index; + final String question; + final String selectedAnswer; + final String correctAnswer; + final bool isCorrect; + + AnsweredQuestion({ + required this.index, + required this.question, + required this.selectedAnswer, + required this.correctAnswer, + required this.isCorrect, + }); + + Map toJson() { + return { + 'index': index, + 'question': question, + 'selectedAnswer': selectedAnswer, + 'correctAnswer': correctAnswer, + 'isCorrect': isCorrect, + }; + } +} diff --git a/lib/feature/quiz_play/view/quiz_play_view.dart b/lib/feature/quiz_play/view/quiz_play_view.dart new file mode 100644 index 0000000..0fe513d --- /dev/null +++ b/lib/feature/quiz_play/view/quiz_play_view.dart @@ -0,0 +1,131 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:quiz_app/component/global_text_field.dart'; +import 'package:quiz_app/feature/quiz_play/controller/quiz_play_controller.dart'; + +class QuizPlayView extends GetView { + const QuizPlayView({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF9FAFB), + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + automaticallyImplyLeading: false, + title: const Text( + 'Kerjakan Soal', + 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(() { + final question = controller.currentQuestion; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LinearProgressIndicator( + value: controller.timeLeft.value / question.duration, + minHeight: 8, + backgroundColor: Colors.grey[300], + valueColor: const AlwaysStoppedAnimation(Color(0xFF2563EB)), + ), + const SizedBox(height: 20), + Text( + 'Soal ${controller.currentIndex.value + 1} dari ${controller.quizData.questionListings.length}', + style: const TextStyle( + fontSize: 16, + color: Colors.grey, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 12), + Text( + question.question, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + const SizedBox(height: 30), + + // Jawaban Berdasarkan Tipe Soal + if (question.type == 'option' && question.options != null) + ...List.generate(question.options!.length, (index) { + final option = question.options![index]; + return Container( + margin: const EdgeInsets.only(bottom: 12), + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.black, + side: const BorderSide(color: Colors.grey), + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + onPressed: () => controller.selectAnswer(option), + child: Text(option), + ), + ); + }) + else if (question.type == 'true_false') + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildTrueFalseButton('Ya', true, controller.choosenAnswerTOF), + _buildTrueFalseButton('Tidak', false, controller.choosenAnswerTOF), + ], + ) + else + GlobalTextField(controller: controller.answerTextController), + const Spacer(), + Obx(() { + return ElevatedButton( + onPressed: controller.nextQuestion, + style: ElevatedButton.styleFrom( + backgroundColor: controller.isAnswerSelected.value ? const Color(0xFF2563EB) : Colors.grey, + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 50), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text( + 'Next', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ); + }), + ], + ); + }), + ), + ), + ); + } + + Widget _buildTrueFalseButton(String label, bool value, RxInt choosenAnswer) { + return Obx(() { + bool isSelected = (choosenAnswer.value == (value ? 1 : 2)); + return ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: isSelected ? (value ? Colors.green[100] : Colors.red[100]) : Colors.white, + foregroundColor: Colors.black, + side: const BorderSide(color: Colors.grey), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + onPressed: () => controller.onChooseTOF(value), + icon: Icon(value ? Icons.check_circle_outline : Icons.cancel_outlined), + label: Text(label), + ); + }); + } +}