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 6981b05..f685ff3 100644 --- a/lib/feature/play_quiz_multiplayer/view/play_quiz_multiplayer.dart +++ b/lib/feature/play_quiz_multiplayer/view/play_quiz_multiplayer.dart @@ -17,7 +17,7 @@ class PlayQuizMultiplayerView extends GetView { } if (controller.currentQuestion.value == null) { - return const Center(child: CircularProgressIndicator()); + return _buildLoadingView(); } return _buildQuestionView(); @@ -26,133 +26,340 @@ class PlayQuizMultiplayerView extends GetView { ); } - Widget _buildQuestionView() { - final question = controller.currentQuestion.value!; - return SafeArea( + Widget _buildLoadingView() { + return Center( child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, children: [ - // Custom AppBar content moved to body - Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - "Soal ${(question.questionIndex)}/10", - style: const TextStyle( - color: Colors.black, - fontWeight: FontWeight.bold, - fontSize: 24, - ), - ), - ), - - 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), + TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: const Duration(milliseconds: 1500), + builder: (context, value, child) { + return Transform.rotate( + angle: value * 6.28, + child: Container( + width: 60, + height: 60, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: const Color(0xFF2563EB), + width: 4, ), - borderRadius: BorderRadius.circular(4), ), - ], - ), - ); - }), - - const SizedBox(height: 20), - Obx(() { - if (controller.isASentAns.value) { - return Container( - padding: const EdgeInsets.all(20), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Add a nice loading animation - - const SizedBox(height: 24), - // Improved text with better styling - const Text( - "Jawaban Anda telah terkirim", - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.blue, - ), - ), - const SizedBox(height: 8), - // Informative subtext - const Text( - "Mohon tunggu soal selanjutnya", - style: TextStyle( - fontSize: 14, - color: Colors.grey, - ), - ), - ], + child: const Center( + child: Icon( + Icons.quiz, + color: Color(0xFF2563EB), + size: 30, + ), ), ), ); - } + }, + ), + const SizedBox(height: 20), + const Text( + "Memuat soal...", + style: TextStyle( + fontSize: 16, + color: Colors.black54, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } - return 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, - ), - ) - ], + Widget _buildQuestionView() { + final question = controller.currentQuestion.value!; + return SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildCustomAppBar(question), + const SizedBox(height: 20), + _buildProgressSection(question), + const SizedBox(height: 20), + _buildQuestionCard(question), + const SizedBox(height: 30), + Expanded(child: _buildAnswerSection()), + _buildSubmitButton(), + ], + ), + ), + ); + } + + Widget _buildCustomAppBar(dynamic question) { + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: const Color(0xFF2563EB).withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + "Soal ${question.questionIndex}/10", + style: const TextStyle( + color: Color(0xFF2563EB), + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ), + Row( + children: [ + Icon( + Icons.people, + size: 20, + color: const Color(0xFF2563EB), + ), + const SizedBox(width: 4), + Text( + "Multiplayer", + style: TextStyle( + color: const Color(0xFF2563EB), + fontWeight: FontWeight.w600, + fontSize: 14, ), ), - ); - }), - // Question content + ], + ), + Obx(() => Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: controller.remainingTime.value <= 10 ? Colors.red.withOpacity(0.1) : const Color(0xFF2563EB).withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: Row( + children: [ + Icon( + Icons.timer_outlined, + size: 16, + color: controller.remainingTime.value <= 10 ? Colors.red : const Color(0xFF2563EB), + ), + const SizedBox(width: 4), + Text( + "${controller.remainingTime.value}s", + style: TextStyle( + color: controller.remainingTime.value <= 10 ? Colors.red : const Color(0xFF2563EB), + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + ], + ), + )), + ], + ), + ); + } + + Widget _buildProgressSection(dynamic question) { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Progress", + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + fontWeight: FontWeight.w500, + ), + ), + Text( + "${(question.questionIndex * 10).toInt()}%", + style: const TextStyle( + fontSize: 14, + color: Color(0xFF2563EB), + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 8), + AnimatedContainer( + duration: const Duration(milliseconds: 300), + height: 8, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: Colors.grey[300], + ), + child: FractionallySizedBox( + alignment: Alignment.centerLeft, + widthFactor: question.questionIndex / 10, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + gradient: const LinearGradient( + colors: [Color(0xFF2563EB), Color(0xFF1E40AF)], + ), + ), + ), + ), + ), + const SizedBox(height: 12), + Obx(() => AnimatedContainer( + duration: const Duration(milliseconds: 100), + height: 4, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2), + color: Colors.grey[300], + ), + child: FractionallySizedBox( + alignment: Alignment.centerLeft, + widthFactor: controller.remainingTime.value / question.duration, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2), + color: controller.remainingTime.value > question.duration * 0.3 + ? Colors.green + : controller.remainingTime.value > question.duration * 0.1 + ? Colors.orange + : Colors.red, + ), + ), + ), + )), + ], + ); + } + + Widget _buildQuestionCard(dynamic question) { + return AnimatedSwitcher( + duration: const Duration(milliseconds: 400), + transitionBuilder: (Widget child, Animation animation) { + return SlideTransition( + position: Tween( + begin: const Offset(0.3, 0), + end: Offset.zero, + ).animate(animation), + child: FadeTransition(opacity: animation, child: child), + ); + }, + child: Container( + key: ValueKey(question.question), + padding: const EdgeInsets.all(20), + width: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 15, + offset: const Offset(0, 5), + ), + ], + ), + child: Text( + question.question, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.black87, + height: 1.4, + ), + ), + ), + ); + } + + Widget _buildAnswerSection() { + return Obx(() { + if (controller.isASentAns.value) { + return _buildWaitingView(); + } + + final question = controller.currentQuestion.value!; + + if (question.type == 'option') return _buildOptionQuestion(); + if (question.type == 'fill_the_blank') return _buildFillInBlankQuestion(); + if (question.type == 'true_false') return _buildTrueFalseQuestion(); + + return const SizedBox(); + }); + } + + Widget _buildWaitingView() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: const Duration(milliseconds: 2000), + builder: (context, value, child) { + return Container( + width: 80, + height: 80, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.green.withOpacity(0.1), + border: Border.all( + color: Colors.green, + width: 3, + ), + ), + child: Transform.scale( + scale: 0.8 + (value * 0.2), + child: const Icon( + Icons.check_circle, + size: 40, + color: Colors.green, + ), + ), + ); + }, + ), + const SizedBox(height: 24), + const Text( + "Jawaban Terkirim!", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.green, + ), + ), + const SizedBox(height: 8), + Text( + "Menunggu soal selanjutnya...", + style: TextStyle( + fontSize: 16, + color: Colors.grey[600], + ), + ), + const SizedBox(height: 20), + Container( + width: 40, + height: 40, + child: CircularProgressIndicator( + strokeWidth: 3, + valueColor: AlwaysStoppedAnimation(Colors.grey[400]!), + ), + ), ], ), ); @@ -162,65 +369,204 @@ class PlayQuizMultiplayerView extends GetView { final options = controller.currentQuestion.value!.options; return Column( children: List.generate(options!.length, (index) { - final option = options[index]; - final isSelected = controller.selectedAnswer.value == index.toString(); - - return Container( - margin: const EdgeInsets.only(bottom: 12), - width: double.infinity, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: isSelected ? const Color(0xFF2563EB) : 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.selectOptionAnswer(index), - child: Text(option), - ), - ); + return _buildOptionButton(options[index], index); }), ); } + Widget _buildOptionButton(String option, int index) { + return Obx(() { + final isSelected = controller.selectedAnswer.value == index.toString(); + + return AnimatedContainer( + duration: const Duration(milliseconds: 200), + margin: const EdgeInsets.only(bottom: 12), + width: double.infinity, + child: Material( + elevation: isSelected ? 8 : 2, + borderRadius: BorderRadius.circular(16), + child: InkWell( + borderRadius: BorderRadius.circular(16), + onTap: () => controller.selectOptionAnswer(index), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 20), + decoration: BoxDecoration( + color: isSelected ? const Color(0xFF2563EB) : Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isSelected ? const Color(0xFF2563EB) : Colors.grey.shade300, + width: isSelected ? 2 : 1, + ), + ), + child: Row( + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 24, + height: 24, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isSelected ? Colors.white : Colors.transparent, + border: Border.all( + color: isSelected ? Colors.white : Colors.grey, + width: 2, + ), + ), + child: isSelected ? const Icon(Icons.check, size: 16, color: Color(0xFF2563EB)) : null, + ), + const SizedBox(width: 16), + Expanded( + child: Text( + option, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: isSelected ? Colors.white : Colors.black87, + ), + ), + ), + ], + ), + ), + ), + ), + ); + }); + } + Widget _buildFillInBlankQuestion() { - return Column( - children: [ - GlobalTextField(controller: controller.fillInAnswerController), - const SizedBox(height: 20), - ], + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 15, + offset: const Offset(0, 5), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Masukkan jawaban Anda:", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.grey[700], + ), + ), + const SizedBox(height: 16), + GlobalTextField(controller: controller.fillInAnswerController), + ], + ), ); } Widget _buildTrueFalseQuestion() { return Column( children: [ - _buildTrueFalseButton('Ya', true), - _buildTrueFalseButton('Tidak', false), + Row( + children: [ + Expanded( + child: _buildTrueFalseButton( + 'Ya', + true, + Icons.check_circle, + Colors.green, + ), + ), + const SizedBox(width: 16), + Expanded( + child: _buildTrueFalseButton( + 'Tidak', + false, + Icons.cancel, + Colors.red, + ), + ), + ], + ), ], ); } - Widget _buildTrueFalseButton(String label, bool value) { - final isSelected = controller.selectedAnswer.value == value.toString(); + Widget _buildTrueFalseButton(String label, bool value, IconData icon, Color color) { + return Obx(() { + final isSelected = controller.selectedAnswer.value == value.toString(); - return Container( - margin: const EdgeInsets.only(bottom: 12), - width: double.infinity, - child: ElevatedButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: isSelected ? (value ? Colors.green : Colors.red) : 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)), + return AnimatedContainer( + duration: const Duration(milliseconds: 200), + height: 120, + child: Material( + elevation: isSelected ? 8 : 2, + borderRadius: BorderRadius.circular(20), + child: InkWell( + borderRadius: BorderRadius.circular(20), + onTap: () => controller.selectTrueFalseAnswer(value), + child: Container( + decoration: BoxDecoration( + color: isSelected ? color.withOpacity(0.1) : Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: isSelected ? color : Colors.grey.shade300, + width: isSelected ? 3 : 1, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isSelected ? color : Colors.grey.shade100, + ), + child: Icon( + icon, + size: 32, + color: isSelected ? Colors.white : Colors.grey, + ), + ), + const SizedBox(height: 12), + Text( + label, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: isSelected ? color : Colors.black87, + ), + ), + ], + ), + ), + ), ), - onPressed: () => controller.selectTrueFalseAnswer(value), - icon: Icon(value ? Icons.check_circle_outline : Icons.cancel_outlined), - label: Text(label), - ), - ); + ); + }); + } + + Widget _buildSubmitButton() { + return Obx(() { + if (controller.isASentAns.value) { + return const SizedBox.shrink(); + } + + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + margin: const EdgeInsets.only(top: 20), + child: GlobalButton( + text: "Kirim Jawaban", + onPressed: controller.submitAnswer, + type: controller.buttonType.value, + ), + ); + }); } Widget _buildDoneView() { @@ -230,32 +576,69 @@ class PlayQuizMultiplayerView extends GetView { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon( - Icons.check_circle, - size: 80, - color: Color(0xFF2563EB), + TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: const Duration(milliseconds: 800), + builder: (context, value, child) { + return Transform.scale( + scale: value, + child: Container( + width: 120, + height: 120, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: const Color(0xFF2563EB).withOpacity(0.1), + border: Border.all( + color: const Color(0xFF2563EB), + width: 4, + ), + ), + child: const Icon( + Icons.emoji_events, + size: 60, + color: Color(0xFF2563EB), + ), + ), + ); + }, ), - const SizedBox(height: 20), + const SizedBox(height: 32), const Text( - "Kuis telah selesai!", - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + "Kuis Selesai!", + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), ), const SizedBox(height: 16), - const Text( - "Terima kasih telah berpartisipasi.", - style: TextStyle(fontSize: 16, color: Colors.black54), + Text( + "Terima kasih telah berpartisipasi dalam kuis multiplayer.", + style: TextStyle( + fontSize: 16, + color: Colors.grey[600], + height: 1.5, + ), textAlign: TextAlign.center, ), const SizedBox(height: 40), - ElevatedButton( - onPressed: controller.goToDetailResult, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF2563EB), - foregroundColor: Colors.white, - minimumSize: const Size(double.infinity, 50), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + Container( + width: double.infinity, + height: 56, + child: ElevatedButton.icon( + onPressed: controller.goToDetailResult, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF2563EB), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + elevation: 6, + ), + icon: const Icon(Icons.assessment), + label: const Text( + "Lihat Hasil", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), ), - child: const Text("Lihat Hasil", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), ), ], ),