From c84133a37216cec14740d78358d99b5e1fd058c1 Mon Sep 17 00:00:00 2001 From: akhdanre Date: Sun, 25 May 2025 15:26:45 +0700 Subject: [PATCH] fix: interface on the quiz result --- assets/translations/en-US.json | 12 +- assets/translations/id-ID.json | 12 +- assets/translations/ms-MY.json | 12 +- .../widget/quiz_item_wa_component.dart | 217 +++++++++++++----- 4 files changed, 192 insertions(+), 61 deletions(-) diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index c41fea5..0da7b79 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -47,12 +47,10 @@ "avg_score": "Average Score", "history_detail_title": "Quiz Detail", - "correct_answer": "Correct", "score": "Score", "time_taken": "Time", "duration_seconds": "{second}s", - "your_answer": "Your answer: {answer}", "question_type_option": "Multiple Choice", "question_type_fill": "Fill in the Blank", "question_type_true_false": "True / False", @@ -105,5 +103,13 @@ "contact_us": "Contact Us", "about_app": "About App", "version": "Version", - "close": "Close" + "close": "Close", + + "your_answer": "Your answer: {answer}", + "correct_answer": "Correct answer: {answer}", + "not_answered": "Not Answered", + "seconds_suffix": "s", + "quiz_type_option": "Multiple Choice", + "quiz_type_true_false": "True or False", + "quiz_type_fill_the_blank": "Fill in the Blank" } diff --git a/assets/translations/id-ID.json b/assets/translations/id-ID.json index 75b6794..da60f76 100644 --- a/assets/translations/id-ID.json +++ b/assets/translations/id-ID.json @@ -47,12 +47,10 @@ "avg_score": "Skor Rata-rata", "history_detail_title": "Detail Kuis", - "correct_answer": "Benar", "score": "Skor", "time_taken": "Waktu", "duration_seconds": "{second} detik", - "your_answer": "Jawaban kamu: {answer}", "question_type_option": "Pilihan Ganda", "question_type_fill": "Isian Kosong", "question_type_true_false": "Benar / Salah", @@ -105,5 +103,13 @@ "contact_us": "Hubungi Kami", "about_app": "Tentang Aplikasi", "version": "Versi", - "close": "Tutup" + "close": "Tutup", + + "your_answer": "Jawabanmu: {answer}", + "correct_answer": "Jawaban benar: {answer}", + "not_answered": "Tidak Menjawab", + "seconds_suffix": "d", + "quiz_type_option": "Pilihan Ganda", + "quiz_type_true_false": "Benar atau Salah", + "quiz_type_fill_the_blank": "Isian Kosong" } diff --git a/assets/translations/ms-MY.json b/assets/translations/ms-MY.json index 4f2558f..fd50edb 100644 --- a/assets/translations/ms-MY.json +++ b/assets/translations/ms-MY.json @@ -47,12 +47,10 @@ "avg_score": "Skor Purata", "history_detail_title": "Butiran Kuiz", - "correct_answer": "Betul", "score": "Skor", "time_taken": "Masa", "duration_seconds": "{second} saat", - "your_answer": "Jawapan anda: {answer}", "question_type_option": "Pilihan Berganda", "question_type_fill": "Isi Tempat Kosong", "question_type_true_false": "Betul / Salah", @@ -102,5 +100,13 @@ "contact_us": "Hubungi Kami", "about_app": "Mengenai Aplikasi", "version": "Versi", - "close": "Tutup" + "close": "Tutup", + + "your_answer": "Jawapan anda: {answer}", + "correct_answer": "Jawapan betul: {answer}", + "not_answered": "Tidak Dijawab", + "seconds_suffix": "s", + "quiz_type_option": "Pilihan Jawapan", + "quiz_type_true_false": "Betul atau Salah", + "quiz_type_fill_the_blank": "Isi Tempat Kosong" } diff --git a/lib/component/widget/quiz_item_wa_component.dart b/lib/component/widget/quiz_item_wa_component.dart index c650fe3..d56d070 100644 --- a/lib/component/widget/quiz_item_wa_component.dart +++ b/lib/component/widget/quiz_item_wa_component.dart @@ -4,16 +4,13 @@ import 'package:lucide_icons/lucide_icons.dart'; import 'package:quiz_app/app/const/colors/app_colors.dart'; import 'package:quiz_app/app/const/text/text_style.dart'; +/// Single quiz result tile. +/// Shows the question, the user's answer, correctness, time spent, and per‑option feedback. +/// +/// * Text strings are fully localised via `easy_localization`. +/// * Long answers now wrap to the next line rather than overflowing. +/// * Option chips highlight both the correct answer and the user's incorrect choice (if any). class QuizItemWAComponent extends StatelessWidget { - final int index; - final String question; - final String type; - final dynamic userAnswer; - final dynamic targetAnswer; - final bool isCorrect; - final double timeSpent; - final List? options; - const QuizItemWAComponent({ super.key, required this.index, @@ -26,20 +23,44 @@ class QuizItemWAComponent extends StatelessWidget { this.options, }); - bool get isOptionType => type == 'option'; + /// One‑based question index. + final int index; + + /// The question text. + final String question; + + /// Question type: `option`, `true_false`, or `fill_the_blank`. + final String type; + + /// Raw user answer (index, bool or string). `-1`/`null` means no answer. + final dynamic userAnswer; + + /// Raw correct answer (index, bool or string). + final dynamic targetAnswer; + + /// Whether the user answered correctly. + final bool isCorrect; + + /// Time spent answering (seconds). + final double timeSpent; + + /// Option texts for option‑type questions. + final List? options; + + bool get _isOptionType => type == 'option'; @override Widget build(BuildContext context) { return Container( width: double.infinity, - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.04), + color: Colors.black.withOpacity(.04), blurRadius: 6, offset: const Offset(0, 2), ), @@ -48,41 +69,89 @@ class QuizItemWAComponent extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // ————————————————— Question text Text( '$index. $question', + softWrap: true, style: AppTextStyles.title.copyWith(fontSize: 16, fontWeight: FontWeight.w600), ), - const SizedBox(height: 16), - if (isOptionType && options != null) _buildOptions(), + + if (_isOptionType && options != null) ...[ + const SizedBox(height: 16), + _OptionsList( + options: options!, + userAnswer: userAnswer, + targetAnswer: targetAnswer, + ), + ], + const SizedBox(height: 12), - _buildAnswerIndicator(context), + _AnswerIndicator( + isCorrect: isCorrect, + isAnswered: userAnswer != null && userAnswer != -1, + userAnswerText: _buildUserAnswerText(), + correctAnswerText: _buildCorrectAnswerText(), + ), const SizedBox(height: 16), const Divider(height: 24, color: AppColors.shadowPrimary), - _buildMetadata(), + _MetaBar(type: type, timeSpent: timeSpent), ], ), ); } - Widget _buildOptions() { + // ——————————————————————————————————————————————————————— Helpers + String _buildUserAnswerText() { + if (userAnswer == null || userAnswer == -1) { + return tr('not_answered'); + } + if (_isOptionType) { + final idx = int.tryParse(userAnswer.toString()) ?? -1; + if (idx >= 0 && idx < (options?.length ?? 0)) return options![idx]; + } + return userAnswer.toString(); + } + + String _buildCorrectAnswerText() { + if (_isOptionType && options != null) { + final idx = int.tryParse(targetAnswer.toString()) ?? -1; + if (idx >= 0 && idx < options!.length) return options![idx]; + } + return targetAnswer.toString(); + } +} + +// ————————————————————————————————————————————————————————— Sub‑widgets + +class _OptionsList extends StatelessWidget { + const _OptionsList({ + required this.options, + required this.userAnswer, + required this.targetAnswer, + }); + + final List options; + final dynamic userAnswer; + final dynamic targetAnswer; + + @override + Widget build(BuildContext context) { return Column( - children: options!.asMap().entries.map((entry) { - final int optIndex = entry.key; - final String text = entry.value; + children: List.generate(options.length, (i) { + final text = options[i]; + final bool isCorrectAnswer = i == targetAnswer; + final bool isUserWrongAnswer = i == userAnswer && !isCorrectAnswer; - final bool isCorrectAnswer = optIndex == targetAnswer; - final bool isUserWrongAnswer = optIndex == userAnswer && !isCorrectAnswer; - - Color? backgroundColor; + Color? bg; IconData icon = LucideIcons.circle; Color iconColor = AppColors.shadowPrimary; if (isCorrectAnswer) { - backgroundColor = AppColors.primaryBlue.withValues(alpha: 0.15); + bg = AppColors.primaryBlue.withOpacity(.15); icon = LucideIcons.checkCircle2; iconColor = AppColors.primaryBlue; } else if (isUserWrongAnswer) { - backgroundColor = Colors.red.withValues(alpha: 0.15); + bg = Colors.red.withOpacity(.15); icon = LucideIcons.xCircle; iconColor = Colors.red; } @@ -92,58 +161,79 @@ class QuizItemWAComponent extends StatelessWidget { margin: const EdgeInsets.symmetric(vertical: 6), padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12), decoration: BoxDecoration( - color: backgroundColor, + color: bg, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.shadowPrimary), ), child: Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon, size: 16, color: iconColor), const SizedBox(width: 8), - Flexible( - child: Text(text, style: AppTextStyles.optionText), + Expanded( + child: Text( + text, + style: AppTextStyles.optionText, + softWrap: true, + ), ), ], ), ); - }).toList(), + }), ); } +} - Widget _buildAnswerIndicator(BuildContext context) { +class _AnswerIndicator extends StatelessWidget { + const _AnswerIndicator({ + required this.isCorrect, + required this.isAnswered, + required this.userAnswerText, + required this.correctAnswerText, + }); + + final bool isCorrect; + final bool isAnswered; + final String userAnswerText; + final String correctAnswerText; + + @override + Widget build(BuildContext context) { final icon = isCorrect ? LucideIcons.checkCircle2 : LucideIcons.xCircle; final color = isCorrect ? AppColors.primaryBlue : Colors.red; - final String userAnswerText; - - if (userAnswer == null || userAnswer == -1) { - userAnswerText = "Tidak Menjawab"; - } else { - userAnswerText = isOptionType ? options![userAnswer] : userAnswer.toString(); - } - - final String correctAnswerText = targetAnswer.toString(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon, color: color, size: 18), const SizedBox(width: 8), - Text( - context.tr('your_answer', namedArgs: {'answer': userAnswerText}), - style: AppTextStyles.statValue, + Expanded( + child: Text( + // "Jawabanmu: " + tr('your_answer', namedArgs: {'answer': userAnswerText}), + style: AppTextStyles.statValue, + softWrap: true, + ), ), ], ), - if (!isCorrect && !isOptionType) ...[ + if (!isCorrect && isAnswered) ...[ const SizedBox(height: 6), Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox(width: 26), - Text( - 'Jawaban benar: $correctAnswerText', - style: AppTextStyles.caption, + const SizedBox(width: 26), // align with text above + Expanded( + child: Text( + // "Jawaban benar: " + tr('correct_answer', namedArgs: {'answer': correctAnswerText}), + style: AppTextStyles.caption, + softWrap: true, + ), ), ], ), @@ -151,18 +241,41 @@ class QuizItemWAComponent extends StatelessWidget { ], ); } +} - Widget _buildMetadata() { +class _MetaBar extends StatelessWidget { + const _MetaBar({required this.type, required this.timeSpent}); + final String type; + final double timeSpent; + + @override + Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - _metaItem(icon: LucideIcons.helpCircle, label: type), - _metaItem(icon: LucideIcons.clock3, label: '${timeSpent.toStringAsFixed(1)}s'), + _MetaItem( + icon: LucideIcons.helpCircle, + label: tr( + 'quiz_type_$type', + ), + ), + _MetaItem( + icon: LucideIcons.clock3, + label: '${timeSpent.toStringAsFixed(1)}${tr('seconds_suffix')}', + ), ], ); } +} - Widget _metaItem({required IconData icon, required String label}) { +class _MetaItem extends StatelessWidget { + const _MetaItem({required this.icon, required this.label}); + + final IconData icon; + final String label; + + @override + Widget build(BuildContext context) { return Row( children: [ Icon(icon, size: 16, color: AppColors.primaryBlue),