fix: interface on the quiz result
This commit is contained in:
parent
a7f6ce8e7e
commit
c84133a372
|
@ -47,12 +47,10 @@
|
||||||
"avg_score": "Average Score",
|
"avg_score": "Average Score",
|
||||||
|
|
||||||
"history_detail_title": "Quiz Detail",
|
"history_detail_title": "Quiz Detail",
|
||||||
"correct_answer": "Correct",
|
|
||||||
"score": "Score",
|
"score": "Score",
|
||||||
"time_taken": "Time",
|
"time_taken": "Time",
|
||||||
"duration_seconds": "{second}s",
|
"duration_seconds": "{second}s",
|
||||||
|
|
||||||
"your_answer": "Your answer: {answer}",
|
|
||||||
"question_type_option": "Multiple Choice",
|
"question_type_option": "Multiple Choice",
|
||||||
"question_type_fill": "Fill in the Blank",
|
"question_type_fill": "Fill in the Blank",
|
||||||
"question_type_true_false": "True / False",
|
"question_type_true_false": "True / False",
|
||||||
|
@ -105,5 +103,13 @@
|
||||||
"contact_us": "Contact Us",
|
"contact_us": "Contact Us",
|
||||||
"about_app": "About App",
|
"about_app": "About App",
|
||||||
"version": "Version",
|
"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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,12 +47,10 @@
|
||||||
"avg_score": "Skor Rata-rata",
|
"avg_score": "Skor Rata-rata",
|
||||||
|
|
||||||
"history_detail_title": "Detail Kuis",
|
"history_detail_title": "Detail Kuis",
|
||||||
"correct_answer": "Benar",
|
|
||||||
"score": "Skor",
|
"score": "Skor",
|
||||||
"time_taken": "Waktu",
|
"time_taken": "Waktu",
|
||||||
"duration_seconds": "{second} detik",
|
"duration_seconds": "{second} detik",
|
||||||
|
|
||||||
"your_answer": "Jawaban kamu: {answer}",
|
|
||||||
"question_type_option": "Pilihan Ganda",
|
"question_type_option": "Pilihan Ganda",
|
||||||
"question_type_fill": "Isian Kosong",
|
"question_type_fill": "Isian Kosong",
|
||||||
"question_type_true_false": "Benar / Salah",
|
"question_type_true_false": "Benar / Salah",
|
||||||
|
@ -105,5 +103,13 @@
|
||||||
"contact_us": "Hubungi Kami",
|
"contact_us": "Hubungi Kami",
|
||||||
"about_app": "Tentang Aplikasi",
|
"about_app": "Tentang Aplikasi",
|
||||||
"version": "Versi",
|
"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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,12 +47,10 @@
|
||||||
"avg_score": "Skor Purata",
|
"avg_score": "Skor Purata",
|
||||||
|
|
||||||
"history_detail_title": "Butiran Kuiz",
|
"history_detail_title": "Butiran Kuiz",
|
||||||
"correct_answer": "Betul",
|
|
||||||
"score": "Skor",
|
"score": "Skor",
|
||||||
"time_taken": "Masa",
|
"time_taken": "Masa",
|
||||||
"duration_seconds": "{second} saat",
|
"duration_seconds": "{second} saat",
|
||||||
|
|
||||||
"your_answer": "Jawapan anda: {answer}",
|
|
||||||
"question_type_option": "Pilihan Berganda",
|
"question_type_option": "Pilihan Berganda",
|
||||||
"question_type_fill": "Isi Tempat Kosong",
|
"question_type_fill": "Isi Tempat Kosong",
|
||||||
"question_type_true_false": "Betul / Salah",
|
"question_type_true_false": "Betul / Salah",
|
||||||
|
@ -102,5 +100,13 @@
|
||||||
"contact_us": "Hubungi Kami",
|
"contact_us": "Hubungi Kami",
|
||||||
"about_app": "Mengenai Aplikasi",
|
"about_app": "Mengenai Aplikasi",
|
||||||
"version": "Versi",
|
"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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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/colors/app_colors.dart';
|
||||||
import 'package:quiz_app/app/const/text/text_style.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 {
|
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<String>? options;
|
|
||||||
|
|
||||||
const QuizItemWAComponent({
|
const QuizItemWAComponent({
|
||||||
super.key,
|
super.key,
|
||||||
required this.index,
|
required this.index,
|
||||||
|
@ -26,20 +23,44 @@ class QuizItemWAComponent extends StatelessWidget {
|
||||||
this.options,
|
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<String>? options;
|
||||||
|
|
||||||
|
bool get _isOptionType => type == 'option';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withValues(alpha: 0.04),
|
color: Colors.black.withOpacity(.04),
|
||||||
blurRadius: 6,
|
blurRadius: 6,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 2),
|
||||||
),
|
),
|
||||||
|
@ -48,41 +69,89 @@ class QuizItemWAComponent extends StatelessWidget {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
// ————————————————— Question text
|
||||||
Text(
|
Text(
|
||||||
'$index. $question',
|
'$index. $question',
|
||||||
|
softWrap: true,
|
||||||
style: AppTextStyles.title.copyWith(fontSize: 16, fontWeight: FontWeight.w600),
|
style: AppTextStyles.title.copyWith(fontSize: 16, fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
if (_isOptionType && options != null) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
if (isOptionType && options != null) _buildOptions(),
|
_OptionsList(
|
||||||
|
options: options!,
|
||||||
|
userAnswer: userAnswer,
|
||||||
|
targetAnswer: targetAnswer,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildAnswerIndicator(context),
|
_AnswerIndicator(
|
||||||
|
isCorrect: isCorrect,
|
||||||
|
isAnswered: userAnswer != null && userAnswer != -1,
|
||||||
|
userAnswerText: _buildUserAnswerText(),
|
||||||
|
correctAnswerText: _buildCorrectAnswerText(),
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Divider(height: 24, color: AppColors.shadowPrimary),
|
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<String> options;
|
||||||
|
final dynamic userAnswer;
|
||||||
|
final dynamic targetAnswer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
children: options!.asMap().entries.map((entry) {
|
children: List.generate(options.length, (i) {
|
||||||
final int optIndex = entry.key;
|
final text = options[i];
|
||||||
final String text = entry.value;
|
final bool isCorrectAnswer = i == targetAnswer;
|
||||||
|
final bool isUserWrongAnswer = i == userAnswer && !isCorrectAnswer;
|
||||||
|
|
||||||
final bool isCorrectAnswer = optIndex == targetAnswer;
|
Color? bg;
|
||||||
final bool isUserWrongAnswer = optIndex == userAnswer && !isCorrectAnswer;
|
|
||||||
|
|
||||||
Color? backgroundColor;
|
|
||||||
IconData icon = LucideIcons.circle;
|
IconData icon = LucideIcons.circle;
|
||||||
Color iconColor = AppColors.shadowPrimary;
|
Color iconColor = AppColors.shadowPrimary;
|
||||||
|
|
||||||
if (isCorrectAnswer) {
|
if (isCorrectAnswer) {
|
||||||
backgroundColor = AppColors.primaryBlue.withValues(alpha: 0.15);
|
bg = AppColors.primaryBlue.withOpacity(.15);
|
||||||
icon = LucideIcons.checkCircle2;
|
icon = LucideIcons.checkCircle2;
|
||||||
iconColor = AppColors.primaryBlue;
|
iconColor = AppColors.primaryBlue;
|
||||||
} else if (isUserWrongAnswer) {
|
} else if (isUserWrongAnswer) {
|
||||||
backgroundColor = Colors.red.withValues(alpha: 0.15);
|
bg = Colors.red.withOpacity(.15);
|
||||||
icon = LucideIcons.xCircle;
|
icon = LucideIcons.xCircle;
|
||||||
iconColor = Colors.red;
|
iconColor = Colors.red;
|
||||||
}
|
}
|
||||||
|
@ -92,58 +161,79 @@ class QuizItemWAComponent extends StatelessWidget {
|
||||||
margin: const EdgeInsets.symmetric(vertical: 6),
|
margin: const EdgeInsets.symmetric(vertical: 6),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
|
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: backgroundColor,
|
color: bg,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(color: AppColors.shadowPrimary),
|
border: Border.all(color: AppColors.shadowPrimary),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, size: 16, color: iconColor),
|
Icon(icon, size: 16, color: iconColor),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Flexible(
|
Expanded(
|
||||||
child: Text(text, style: AppTextStyles.optionText),
|
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 icon = isCorrect ? LucideIcons.checkCircle2 : LucideIcons.xCircle;
|
||||||
final color = isCorrect ? AppColors.primaryBlue : Colors.red;
|
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(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, color: color, size: 18),
|
Icon(icon, color: color, size: 18),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Expanded(
|
||||||
context.tr('your_answer', namedArgs: {'answer': userAnswerText}),
|
child: Text(
|
||||||
|
// "Jawabanmu: <answer>"
|
||||||
|
tr('your_answer', namedArgs: {'answer': userAnswerText}),
|
||||||
style: AppTextStyles.statValue,
|
style: AppTextStyles.statValue,
|
||||||
|
softWrap: true,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (!isCorrect && !isOptionType) ...[
|
if (!isCorrect && isAnswered) ...[
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Row(
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(width: 26),
|
const SizedBox(width: 26), // align with text above
|
||||||
Text(
|
Expanded(
|
||||||
'Jawaban benar: $correctAnswerText',
|
child: Text(
|
||||||
|
// "Jawaban benar: <answer>"
|
||||||
|
tr('correct_answer', namedArgs: {'answer': correctAnswerText}),
|
||||||
style: AppTextStyles.caption,
|
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(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
_metaItem(icon: LucideIcons.helpCircle, label: type),
|
_MetaItem(
|
||||||
_metaItem(icon: LucideIcons.clock3, label: '${timeSpent.toStringAsFixed(1)}s'),
|
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(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, size: 16, color: AppColors.primaryBlue),
|
Icon(icon, size: 16, color: AppColors.primaryBlue),
|
||||||
|
|
Loading…
Reference in New Issue