diff --git a/app/lib/features/quiz_exercise/presentation/pages/v2/_pages.dart b/app/lib/features/quiz_exercise/presentation/pages/v2/_pages.dart new file mode 100644 index 0000000..003c08d --- /dev/null +++ b/app/lib/features/quiz_exercise/presentation/pages/v2/_pages.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../../core/bases/widgets/layout/bebras_scaffold.dart'; +import '../../../../quiz_download/presentation/bloc/quiz_registration_cubit.dart'; +import '../../../../quiz_start/presentation/bloc/quiz_start_cubit.dart'; +import '../../bloc/quiz_exercise_cubit.dart'; + +import 'task_dialog.dart'; +import 'task_view.dart'; + +part 'quiz_exercise_page.dart'; diff --git a/app/lib/features/quiz_exercise/presentation/pages/v2/quiz_exercise_page.dart b/app/lib/features/quiz_exercise/presentation/pages/v2/quiz_exercise_page.dart new file mode 100644 index 0000000..218d276 --- /dev/null +++ b/app/lib/features/quiz_exercise/presentation/pages/v2/quiz_exercise_page.dart @@ -0,0 +1,209 @@ +part of '_pages.dart'; + +class QuizExercisePageV2 extends StatefulWidget { + final String? quizParticipantId; + const QuizExercisePageV2({super.key, this.quizParticipantId}); + + @override + State createState() => _QuizExercisePageV2State(); +} + +class _QuizExercisePageV2State extends State { + @override + void initState() { + final cubit = context.read(); + if (cubit.quizParticipantId == widget.quizParticipantId && + cubit.state is QuizExercisePaused) { + cubit.resume(); + } else { + cubit.initialize(quizParticipantId: widget.quizParticipantId); + } + super.initState(); + } + + @override + void dispose() { + context.read().pause(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BebrasScaffold( + avoidBottomInset: false, + body: SingleChildScrollView( + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.only( + left: 15, + top: 20, + right: 16, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: const Icon(Icons.arrow_back), + ), + const Flexible( + child: Center( + child: Text( + 'Bebras Pandai', + textAlign: TextAlign.center, + style: + TextStyle(fontWeight: FontWeight.bold, fontSize: 16) + ), + ), + ), + GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: const Icon(Icons.list), + ), + ], + ), + const SizedBox( + height: 10, + ), + BlocConsumer( + listenWhen: (context, state) { + return state is QuizExerciseFinished; + }, listener: (context, state) { + if (state is QuizExerciseFinished) { + context.pushReplacement( + Uri( + path: '/quiz_result', + queryParameters: { + 'quiz_participant_id': state.quizParticipantId, + }, + ).toString(), + ); + context + .read() + .initialize(widget.quizParticipantId); + context + .read() + .fetchParticipantWeeklyQuiz(); + } + }, + buildWhen: (context, state) { + return state is! QuizExerciseFinished; + }, + builder: (context, state) { + if (state is QuizExerciseLoading) { + return const Center( + child: CircularProgressIndicator(), + ); + } + if (state is QuizExerciseFailed) { + return Text(state.error); + } + if (state is QuizExerciseShow) { + return TaskView( + task: state.quizExercise, + context: context, + remainingDuration: state.remainingDuration, + attempt: state.attempt, + onTaskTap: () { + onAnswerTap(); + }, + showPreviousButton: state.currentProblemIndex > 0, + showNextButton: state.currentProblemIndex < + (state.totalProblem - 1), + ); + } + return Container(); + } + ), + ], + ), + ), + ], + ), + ), + ); + } + + Future onAnswerTap() { + return showModalBottomSheet( + context: context, + builder: (BuildContext context) { + return BlocBuilder( + buildWhen: (context, state) { + return state is QuizExerciseShow; + }, + builder: (context, state) { + if (state is QuizExerciseShow) { + return StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return TaskDialog( + task: state.quizExercise, + preview: false, + answer: state.answer.answer, + error: state.modalErrorMessage, + ); + // return Container( + // height: 200, + // width: double.infinity, + // decoration: const BoxDecoration( + // color: Colors.white, + // borderRadius: BorderRadius.only( + // topLeft: Radius.circular(42), + // topRight: Radius.circular(42), + // ), + // ), + // child: TaskDialog( + // task: state.quizExercise, + // preview: false, + // answer: state.answer.answer, + // error: state.modalErrorMessage, + // ), + // ); + } + ); + } else { + return const SizedBox( + width: 100, + height: 100, + ); + } + }, + ); + }, +); + + } + + // void onAnswerTap() { + // showDialog( + // context: context, + // builder: (context) { + // return BlocBuilder( + // buildWhen: (context, state) { + // return state is QuizExerciseShow; + // }, + // builder: (context, state) { + // if (state is QuizExerciseShow) { + // return TaskDialog( + // task: state.quizExercise, + // preview: false, + // answer: state.answer.answer, + // error: state.modalErrorMessage, + // ); + // } + // return const SizedBox( + // width: 100, + // height: 100, + // ); + // }); + // } + // ); + // } +} \ No newline at end of file diff --git a/app/lib/features/quiz_exercise/presentation/pages/v2/task_dialog.dart b/app/lib/features/quiz_exercise/presentation/pages/v2/task_dialog.dart new file mode 100644 index 0000000..4eea9e4 --- /dev/null +++ b/app/lib/features/quiz_exercise/presentation/pages/v2/task_dialog.dart @@ -0,0 +1,132 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../../../core/bases/widgets/atoms/button.dart'; +import '../../../../../core/bases/widgets/atoms/html_cached_image.dart'; +import '../../../../../core/constants/assets.dart'; +import '../../../../../core/theme/base_colors.dart'; +import '../../../../authentication/register/presentation/widgets/custom_text_field.dart'; +import '../../bloc/quiz_exercise_cubit.dart'; +import '../../model/quiz_exercise.dart'; + +class TaskDialog extends StatelessWidget { + final String? answer; + final String? error; + final bool preview; + final QuizExercise task; + const TaskDialog({ + required this.task, + required this.preview, + super.key, + this.answer, + this.error, + }); + + @override + Widget build(BuildContext context) { + return Container( + constraints: const BoxConstraints(minHeight: 30), + width: double.infinity, + color: Colors.white, + child: SizedBox( + height: MediaQuery.of(context).size.height, + child: Column( + children: [ + if (task.type == 'MULTIPLE_CHOICE') + ...task.question.options.asMap().entries.map((e) { + final current = String.fromCharCode(65 + e.key); + + return RadioListTile( + title: SizedBox( + child: Transform.translate( + offset: const Offset(-20, 0), + child: Row( + children: [ + Text('$current. '), + task.type == 'MULTIPLE_CHOICE_IMAGE' + ? Image.network( + e.value.content.replaceAll( + Assets.sourceImg, Assets.urlImg), + width: MediaQuery.of(context).size.width - 240, + ) + : Flexible( + child: HtmlWithCachedImages(data: e.value.content), + ) + ], + ), + ), + ), + value: e.value.id, + groupValue: answer, + onChanged: (value) { + context.read().selectAnswer(e.value.id); + }, + ); + }), + if (task.type == 'SHORT_ANSWER') + Container( + padding: const EdgeInsets.only(top: 20), + child: CustomTextField( + 'Jawaban anda', + (value) { + context.read().fillAnswer(value); + }, + (p0) => null, + answer, + ), + ), + if (!preview) if (error != null) Text(error!), + Padding( + padding: const EdgeInsets.only( + left: 25, + right: 25, + ), + child: Expanded( + child: Button( + innerHorizontalPadding: 4, + innerVerticalPadding: 8, + fontSize: 13, + text: 'Pilih Jawaban', + customButtonColor: BaseColors.brightBlue, + customTextColor: Colors.white, + borderRadius: 4, + onTap: () { + var error = ''; + if (task.type == 'SHORT_ANSWER') { + if (answer == '') { + error = 'Isi jawaban anda'; + } + } else { + if (answer == '') { + error = 'Pilih salah satu jawaban'; + } + } + + if (error != '') { + final snackBar = SnackBar( + backgroundColor: Colors.red, + duration: const Duration(seconds: 1), + behavior: SnackBarBehavior.floating, + margin: const EdgeInsets.only( + bottom: 50, left: 10, right: 10), + content: Text(error), + ); + + ScaffoldMessenger.of(context).showSnackBar(snackBar); + + error = ''; + return; + } + + context.read().submitAnswer(); + Navigator.pop(context); + }, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/app/lib/features/quiz_exercise/presentation/pages/v2/task_view.dart b/app/lib/features/quiz_exercise/presentation/pages/v2/task_view.dart new file mode 100644 index 0000000..b6a547d --- /dev/null +++ b/app/lib/features/quiz_exercise/presentation/pages/v2/task_view.dart @@ -0,0 +1,249 @@ +// ignore_for_file: lines_longer_than_80_chars +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../../../core/bases/enum/button_type.dart'; +import '../../../../../core/bases/widgets/atoms/button.dart'; +import '../../../../../core/bases/widgets/atoms/html_cached_image.dart'; +import '../../../../../core/constants/assets.dart'; +import '../../../../../core/theme/base_colors.dart'; +import '../../bloc/quiz_exercise_cubit.dart'; +import '../../model/quiz_exercise.dart'; +import '../../model/quiz_exercise_answer.dart'; +import '../../model/quiz_exercise_attempt.dart'; + +class TaskView extends StatelessWidget { + final Duration? remainingDuration; + final QuizExercise task; + final QuizExerciseAttempt attempt; + final BuildContext context; + final Function onTaskTap; + final bool showPreviousButton; + final bool showNextButton; + const TaskView({ + required this.task, + required this.context, + required this.onTaskTap, + required this.attempt, + super.key, + this.showPreviousButton = false, + this.showNextButton = false, + this.remainingDuration, + }); + + Future _showExitConfirmationDialog(BuildContext context) async { + return await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Anda akan keluar dari latihan?'), + content: const Text( + 'Latihan akan dianggap selesai dengan hasil seadanya'), + actions: [ + SizedBox( + width: 100, + height: 50, + child: Button( + buttonType: ButtonType.secondary, + text: 'Cancel', + onTap: () { + Navigator.of(context).pop(false); + }, + ), + ), + SizedBox( + width: 100, + height: 50, + child: Button( + buttonType: ButtonType.tertiary, + text: 'Ya', + onTap: () { + context + .read() + .finishExerciseTimeUp(); + Navigator.of(context).pop(true); + })) + ], + ), + ) ?? + false; + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async { + // Display the popup when the back button is pressed + final result = await _showExitConfirmationDialog(context); + return result; + }, + child: Column( + children: [ + if (remainingDuration != null) + Container( + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + decoration: BoxDecoration( + border: Border.all( + color: BaseColors.brightBlue, + ), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + '${remainingDuration!.inMinutes.toString().padLeft(2, '0')} : ${remainingDuration!.inSeconds.remainder(60).toString().padLeft(2, '0')}', + style: const TextStyle( + color: BaseColors.brightBlue, + fontSize: 11, + fontWeight: FontWeight.bold, + ) + )), + if (remainingDuration != null) + const SizedBox( + height: 10, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Image.asset( + '${Assets.flagDir}${task.country}.png', + width: 20, + height: 10, + ), + Text( + task.title, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + task.source, + style: const TextStyle(fontSize: 7), + ), + Text( + task.id, + style: const TextStyle(fontSize: 9), + ), + ], + ), + ], + ), + const SizedBox( + height: 3, + ), + Container( + height: MediaQuery.of(context).size.height - 280, + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + color: const Color(0xFFF2FBFE), + borderRadius: BorderRadius.circular(10), + ), + child: SingleChildScrollView( + child: HtmlWithCachedImages( + data: task.description.content + .replaceAll(Assets.sourceImg, Assets.urlImg), + ), + ), + ), + const SizedBox( + height: 14, + ), + Button( + innerHorizontalPadding: 4, + innerVerticalPadding: 8, + fontSize: 13, + text: 'Jawab', + customButtonColor: BaseColors.brightBlue, + customTextColor: Colors.white, + borderRadius: 4, + onTap: onTaskTap, + ), + const SizedBox( + height: 20, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Column( + children: [ + if (showPreviousButton) + Button( + innerHorizontalPadding: 4, + innerVerticalPadding: 8, + fontSize: 13, + text: 'Sebelumnya', + customBorderColor: BaseColors.brightBlue, + customButtonColor: Colors.white, + customTextColor: BaseColors.brightBlue, + borderRadius: 4, + onTap: () { + context.read().toPreviousQuestion(); + }, + ), + ], + ), + ), + const SizedBox( + width: 30, + ), + Flexible( + child: Column( + children: [ + if (showNextButton && attempt.totalBlank != 0) + Button( + innerHorizontalPadding: 4, + innerVerticalPadding: 8, + fontSize: 13, + text: 'Selanjutnya', + customButtonColor: BaseColors.brightBlue, + customTextColor: Colors.white, + borderRadius: 4, + onTap: () { + context.read().toNextQuestion(); + }, + ), + if (attempt.totalBlank == 1) + Button( + innerHorizontalPadding: 4, + innerVerticalPadding: 8, + fontSize: 13, + text: 'Selesai', + customButtonColor: BaseColors.grey, + customTextColor: Colors.white, + borderRadius: 4, + isDisabled: true, + onTap: () { + context.read().finishExercise(); + }, + ), + if (attempt.totalBlank == 0) + Button( + innerHorizontalPadding: 4, + innerVerticalPadding: 8, + fontSize: 13, + text: 'Selesai', + customButtonColor: BaseColors.brightBlue, + customTextColor: Colors.white, + borderRadius: 4, + onTap: () { + context.read().finishExercise(); + }, + ), + ], + ), + ), + ], + ), + ], + )); + } +} diff --git a/app/lib/services/router_service.dart b/app/lib/services/router_service.dart index 38fcde5..c3563d7 100644 --- a/app/lib/services/router_service.dart +++ b/app/lib/services/router_service.dart @@ -13,6 +13,7 @@ import '../features/onboarding/presentation/pages/_pages.dart'; import '../features/onboarding/presentation/pages/v2/_pages.dart'; import '../features/quiz_download/presentation/pages/_pages.dart'; import '../features/quiz_exercise/presentation/pages/_pages.dart'; +import '../features/quiz_exercise/presentation/pages/v2/_pages.dart'; import '../features/quiz_registration/presentation/pages/_pages.dart'; import '../features/quiz_result/presentation/pages/_pages.dart'; import '../features/quiz_start/presentation/pages/_pages.dart'; @@ -133,7 +134,9 @@ GoRouter router = GoRouter( quizParticipantId: state.queryParameters['quiz_participant_id'], ); } else if (dotenv.env['APP_VERSION'] == 'V2') { - // return const V2QuizExercisePage(); + return QuizExercisePageV2( + quizParticipantId: state.queryParameters['quiz_participant_id'], + ); } return QuizExercisePage( quizParticipantId: state.queryParameters['quiz_participant_id'],