fix: interface on the quiz result
This commit is contained in:
parent
a7f6ce8e7e
commit
c84133a372
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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<String>? 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<String>? 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<String> 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: <answer>"
|
||||
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: <answer>"
|
||||
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),
|
||||
|
|
Loading…
Reference in New Issue