diff --git a/lib/data/services/socket_service.dart b/lib/data/services/socket_service.dart index 7f39b17..7403c3e 100644 --- a/lib/data/services/socket_service.dart +++ b/lib/data/services/socket_service.dart @@ -119,9 +119,9 @@ class SocketService { }); } - void startQuiz({required String sessionCode}) { + void startQuiz({required String sessionId}) { socket.emit('start_quiz', { - 'session_code': sessionCode, + 'session_id': sessionId, }); } @@ -129,6 +129,7 @@ class SocketService { required String sessionId, required String userId, required int questionIndex, + required int timeSpent, required dynamic answer, }) { socket.emit('submit_answer', { @@ -136,6 +137,7 @@ class SocketService { 'user_id': userId, 'question_index': questionIndex, 'answer': answer, + 'time_spent': timeSpent, }); } diff --git a/lib/feature/monitor_quiz/controller/monitor_quiz_controller.dart b/lib/feature/monitor_quiz/controller/monitor_quiz_controller.dart index f4946d8..62f1c1e 100644 --- a/lib/feature/monitor_quiz/controller/monitor_quiz_controller.dart +++ b/lib/feature/monitor_quiz/controller/monitor_quiz_controller.dart @@ -46,33 +46,58 @@ class MonitorQuizController extends GetxController { _socketService.scoreUpdates.listen((data) { logC.i("📊 Score Update Received: $data"); - final Map scoreMap = Map.from(data); + // Ensure data is a valid map + // if (data is! Map) { + // logC.e("Invalid score update format: $data"); + // return; + // } - scoreMap.forEach((userId, scoreData) { + // Parse the score data more carefully + final List scoreList = data['scores'] ?? []; + + for (var scoreData in scoreList) { + // Safely extract user ID and score information + final String? userId = scoreData['user_id']; + + if (userId == null) { + logC.w("Skipping score update with missing user ID"); + continue; + } + + // Find the index of the participant final index = participan.indexWhere((p) => p.id == userId); if (index != -1) { // Participant found, update their scores final participant = participan[index]; - final correct = scoreData["correct"] ?? 0; - final incorrect = scoreData["incorrect"] ?? 0; + // Safely extract correct and incorrect values, default to 0 + final int correct = scoreData['correct'] ?? 0; + final int incorrect = scoreData['incorrect'] ?? 0; + final int totalScore = scoreData['total_score'] ?? 0; + + // Update participant scores participant.correct.value = correct; participant.wrong.value = incorrect; + participant.totalScore.value = totalScore; // Assuming you have a totalScore observable } else { - // Participant not found, optionally add new participant + // Participant not found, add new participant participan.add( ParticipantAnswerPoint( id: userId, - name: "Unknown", // Or fetch proper name if available - correct: (scoreData["correct"] ?? 0).obs, - wrong: (scoreData["incorrect"] ?? 0).obs, + name: "Unknown", // Consider fetching proper name if possible + correct: (scoreData['correct'] ?? 0).obs, + wrong: (scoreData['incorrect'] ?? 0).obs, + totalScore: (scoreData['total_score'] ?? 0).obs, ), ); } - }); + } - // Notify observers if needed (optional) + // Sort participants by total score (optional) + participan.sort((a, b) => b.totalScore.value.compareTo(a.totalScore.value)); + + // Notify observers participan.refresh(); }); } @@ -83,12 +108,15 @@ class ParticipantAnswerPoint { final String name; final RxInt correct; final RxInt wrong; + final RxInt totalScore; ParticipantAnswerPoint({ required this.id, required this.name, RxInt? correct, RxInt? wrong, + RxInt? totalScore, }) : correct = correct ?? 0.obs, - wrong = wrong ?? 0.obs; + wrong = wrong ?? 0.obs, + totalScore = totalScore ?? 0.obs; } diff --git a/lib/feature/play_quiz_multiplayer/controller/play_quiz_controller.dart b/lib/feature/play_quiz_multiplayer/controller/play_quiz_controller.dart index f0881b7..1a20595 100644 --- a/lib/feature/play_quiz_multiplayer/controller/play_quiz_controller.dart +++ b/lib/feature/play_quiz_multiplayer/controller/play_quiz_controller.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:quiz_app/component/global_button.dart'; @@ -7,21 +8,21 @@ import 'package:quiz_app/data/services/socket_service.dart'; class PlayQuizMultiplayerController extends GetxController { final SocketService _socketService; final UserController _userController; - PlayQuizMultiplayerController(this._socketService, this._userController); - // final questions = [].obs; final Rxn currentQuestion = Rxn(); final currentQuestionIndex = 0.obs; final selectedAnswer = Rxn(); final isDone = false.obs; - final Rx buttonType = ButtonType.disabled.obs; - final fillInAnswerController = TextEditingController(); RxBool isASentAns = false.obs; - late final String sessionCode; + // Timer related variables + final RxInt remainingTime = 0.obs; + Timer? _timer; + + late final String sessionId; late final bool isAdmin; @override @@ -33,14 +34,13 @@ class PlayQuizMultiplayerController extends GetxController { _loadData() { final args = Get.arguments as Map; - sessionCode = args["session_code"]; + sessionId = args["session_id"]; isAdmin = args["is_admin"]; } _registerListener() { fillInAnswerController.addListener(() { final text = fillInAnswerController.text; - if (text.isNotEmpty) { buttonType.value = ButtonType.primary; } else { @@ -52,17 +52,52 @@ class PlayQuizMultiplayerController extends GetxController { buttonType.value = ButtonType.disabled; fillInAnswerController.clear(); isASentAns.value = false; + selectedAnswer.value = null; final model = MultiplayerQuestionModel.fromJson(Map.from(data)); currentQuestion.value = model; - fillInAnswerController.clear(); + + // Start the timer for this question + _startTimer(model.duration); }); _socketService.quizDone.listen((_) { isDone.value = true; + // Cancel timer when quiz is done + _cancelTimer(); }); } + // Start timer with the question duration + void _startTimer(int duration) { + // Cancel any existing timer + _cancelTimer(); + + // Set initial remaining time in seconds + remainingTime.value = duration; + + // Create a timer that ticks every second + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (remainingTime.value > 0) { + remainingTime.value--; + } else { + // Time's up - cancel the timer + _cancelTimer(); + + // Auto-submit if the user hasn't already submitted an answer + if (!isASentAns.value) { + submitAnswer(); + } + } + }); + } + + // Cancel the timer + void _cancelTimer() { + _timer?.cancel(); + _timer = null; + } + void selectOptionAnswer(String option) { selectedAnswer.value = option; buttonType.value = ButtonType.primary; @@ -76,8 +111,8 @@ class PlayQuizMultiplayerController extends GetxController { void submitAnswer() { final question = currentQuestion.value!; final type = question.type; - String? answer; + if (type == 'fill_the_blank') { answer = fillInAnswerController.text.trim(); } else { @@ -86,10 +121,11 @@ class PlayQuizMultiplayerController extends GetxController { if (answer != null && answer.isNotEmpty) { _socketService.sendAnswer( - sessionId: sessionCode, + sessionId: sessionId, userId: _userController.userData!.id, questionIndex: question.questionIndex, answer: answer, + timeSpent: question.duration - remainingTime.value, ); isASentAns.value = true; } @@ -98,6 +134,7 @@ class PlayQuizMultiplayerController extends GetxController { @override void onClose() { fillInAnswerController.dispose(); + _cancelTimer(); // Important: cancel timer when controller is closed super.onClose(); } } diff --git a/lib/feature/play_quiz_multiplayer/view/play_quiz_multiplayer.dart b/lib/feature/play_quiz_multiplayer/view/play_quiz_multiplayer.dart index 42faa29..55292a6 100644 --- a/lib/feature/play_quiz_multiplayer/view/play_quiz_multiplayer.dart +++ b/lib/feature/play_quiz_multiplayer/view/play_quiz_multiplayer.dart @@ -9,15 +9,7 @@ class PlayQuizMultiplayerView extends GetView { Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFFF9FAFB), - appBar: AppBar( - backgroundColor: Colors.transparent, - elevation: 0, - centerTitle: true, - title: Text( - "Soal ${(controller.currentQuestion.value?.questionIndex ?? 0)}/10", - style: const TextStyle(color: Colors.black, fontWeight: FontWeight.bold), - ), - ), + // Remove the AppBar and put everything in the body body: Obx(() { if (controller.isDone.value) { return _buildDoneView(); @@ -37,27 +29,110 @@ class PlayQuizMultiplayerView extends GetView { Widget _buildQuestionView() { final question = controller.currentQuestion.value!; - return Padding( - padding: const EdgeInsets.all(20.0), + return SafeArea( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - question.question, - style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black), - ), - const SizedBox(height: 20), - if (question.type == 'option') _buildOptionQuestion(), - if (question.type == 'fill_the_blank') _buildFillInBlankQuestion(), - if (question.type == 'true_false') _buildTrueFalseQuestion(), - const Spacer(), - Obx( - () => GlobalButton( - text: "Kirim jawaban", - onPressed: controller.submitAnswer, - type: controller.buttonType.value, + // Custom AppBar content moved to body + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Back button + IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => Get.back(), + ), + // Title + Text( + "Soal ${(question.questionIndex + 1)}/10", + style: const TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + // Empty container for spacing + Container(width: 48), + ], ), - ) + ), + + // Timer progress bar + Obx(() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Time remaining text + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + "Waktu tersisa:", + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.black54, + ), + ), + Text( + "${controller.remainingTime.value} detik", + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: controller.remainingTime.value <= 10 ? Colors.red : const Color(0xFF2563EB), + ), + ), + ], + ), + const SizedBox(height: 8), + // Progress bar + LinearProgressIndicator( + value: controller.remainingTime.value / question.duration, + minHeight: 8, + backgroundColor: Colors.grey[300], + valueColor: AlwaysStoppedAnimation( + controller.remainingTime.value <= 10 ? Colors.red : const Color(0xFF2563EB), + ), + borderRadius: BorderRadius.circular(4), + ), + ], + ), + ); + }), + + const SizedBox(height: 20), + + // Question content + Expanded( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + question.question, + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black), + ), + const SizedBox(height: 20), + if (question.type == 'option') _buildOptionQuestion(), + if (question.type == 'fill_the_blank') _buildFillInBlankQuestion(), + if (question.type == 'true_false') _buildTrueFalseQuestion(), + const Spacer(), + Obx( + () => GlobalButton( + text: "Kirim jawaban", + onPressed: controller.submitAnswer, + type: controller.buttonType.value, + ), + ) + ], + ), + ), + ), ], ), ); @@ -129,40 +204,45 @@ class PlayQuizMultiplayerView extends GetView { } Widget _buildDoneView() { - return Padding( - padding: const EdgeInsets.all(20), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - "Kuis telah selesai!", - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - // Arahkan ke halaman hasil atau leaderboard - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF2563EB), - foregroundColor: Colors.white, - minimumSize: const Size(double.infinity, 50), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + return SafeArea( + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.check_circle, + size: 80, + color: Color(0xFF2563EB), ), - child: const Text("Lihat Hasil", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), - ), - ], + const SizedBox(height: 20), + const Text( + "Kuis telah selesai!", + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + const Text( + "Terima kasih telah berpartisipasi.", + style: TextStyle(fontSize: 16, color: Colors.black54), + textAlign: TextAlign.center, + ), + const SizedBox(height: 40), + ElevatedButton( + onPressed: () { + // Arahkan ke halaman hasil atau leaderboard + Get.back(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF2563EB), + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 50), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + child: const Text("Lihat Hasil", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + ), + ], + ), ), ); } - - // Widget _buildProgressBar() { - // final question = controller.currentQuestion; - // return LinearProgressIndicator( - // value: controller.timeLeft.value / question.duration, - // minHeight: 8, - // backgroundColor: Colors.grey[300], - // valueColor: const AlwaysStoppedAnimation(Color(0xFF2563EB)), - // ); - // } } diff --git a/lib/feature/waiting_room/controller/waiting_room_controller.dart b/lib/feature/waiting_room/controller/waiting_room_controller.dart index 6295739..2ec10e9 100644 --- a/lib/feature/waiting_room/controller/waiting_room_controller.dart +++ b/lib/feature/waiting_room/controller/waiting_room_controller.dart @@ -16,6 +16,7 @@ class WaitingRoomController extends GetxController { WaitingRoomController(this._socketService, this._userController); final sessionCode = ''.obs; + String sessionId = ''; final quizMeta = Rx(null); final joinedUsers = [].obs; final isAdmin = true.obs; @@ -38,6 +39,7 @@ class WaitingRoomController extends GetxController { isAdmin.value = data.isAdmin; sessionCode.value = roomData!.sessionCode; + sessionId = roomData!.sessionId; quizMeta.value = data.quizInfo; @@ -85,7 +87,7 @@ class WaitingRoomController extends GetxController { Get.snackbar("Info", "Kuis telah dimulai"); if (!isAdmin.value) { Get.offAllNamed(AppRoutes.playQuizMPLPage, arguments: { - "session_code": sessionCode.value, + "session_id": sessionId, "is_admin": isAdmin.value, }); } @@ -104,9 +106,9 @@ class WaitingRoomController extends GetxController { } void startQuiz() { - _socketService.startQuiz(sessionCode: sessionCode.value); + _socketService.startQuiz(sessionId: sessionId); Get.offAllNamed(AppRoutes.monitorQuizMPLPage, arguments: { - "session_code": sessionCode.value, + "session_id": sessionId, "is_admin": isAdmin.value, "list_participan": joinedUsers.toList(), });