feat: done on quiz result

This commit is contained in:
akhdanre 2025-04-28 20:58:45 +07:00
parent 9797fd4a4f
commit 51182b8c7b
6 changed files with 323 additions and 224 deletions

View File

@ -0,0 +1,194 @@
import 'package:flutter/material.dart';
import 'package:quiz_app/app/const/colors/app_colors.dart';
import 'package:quiz_app/app/const/enums/question_type.dart';
import 'package:quiz_app/data/models/quiz/quiestion_data_model.dart';
import 'package:quiz_app/feature/quiz_play/controller/quiz_play_controller.dart';
class QuestionContainerWidget extends StatelessWidget {
final QuestionData question;
final AnsweredQuestion? answeredQuestion;
const QuestionContainerWidget({super.key, required this.question, this.answeredQuestion});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 20),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.borderLight),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 6,
offset: const Offset(2, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Soal ${question.index}', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: AppColors.darkText)),
const SizedBox(height: 6),
Text(
_mapQuestionTypeToText(question.type),
style: const TextStyle(fontSize: 12, color: AppColors.softGrayText, fontStyle: FontStyle.italic),
),
const SizedBox(height: 12),
Text(
question.question ?? '-',
style: const TextStyle(fontSize: 16, color: AppColors.darkText),
),
const SizedBox(height: 16),
_buildAnswerSection(question),
if (answeredQuestion != null) ...[
const SizedBox(height: 16),
_buildAnsweredSection(answeredQuestion!),
],
const SizedBox(height: 10),
const Text(
'Durasi: 0 detik',
style: TextStyle(fontSize: 14, color: AppColors.softGrayText),
),
],
),
);
}
Widget _buildAnswerSection(QuestionData question) {
if (question.type == QuestionType.option && question.options != null) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: question.options!.map((option) {
bool isCorrect = question.correctAnswerIndex == option.index;
return Container(
margin: const EdgeInsets.symmetric(vertical: 4),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: BoxDecoration(
color: isCorrect ? AppColors.primaryBlue.withOpacity(0.1) : Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(
isCorrect ? Icons.check_circle_rounded : Icons.circle_outlined,
size: 18,
color: isCorrect ? AppColors.primaryBlue : AppColors.softGrayText,
),
const SizedBox(width: 8),
Expanded(
child: Text(
option.text,
style: TextStyle(
fontWeight: isCorrect ? FontWeight.bold : FontWeight.normal,
color: isCorrect ? AppColors.primaryBlue : AppColors.darkText,
),
),
),
],
),
);
}).toList(),
);
} else if (question.type == QuestionType.fillTheBlank) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildFillTheBlankPossibilities(question.answer ?? '-'),
);
} else if (question.type == QuestionType.trueOrFalse) {
return Text(
'Jawaban: ${question.answer ?? '-'}',
style: const TextStyle(color: AppColors.softGrayText),
);
} else {
return const SizedBox();
}
}
Widget _buildAnsweredSection(AnsweredQuestion answered) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Jawaban Anda:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: AppColors.darkText)),
const SizedBox(height: 6),
Row(
children: [
Icon(
answered.isCorrect ? Icons.check_circle : Icons.cancel,
color: answered.isCorrect ? Colors.green : Colors.red,
size: 20,
),
const SizedBox(width: 8),
Expanded(
child: Text(
answered.selectedAnswer,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: answered.isCorrect ? Colors.green : Colors.red,
),
),
),
],
),
],
);
}
String _mapQuestionTypeToText(QuestionType? type) {
switch (type) {
case QuestionType.option:
return 'Tipe: Pilihan Ganda';
case QuestionType.fillTheBlank:
return 'Tipe: Isian Kosong';
case QuestionType.trueOrFalse:
return 'Tipe: Benar / Salah';
default:
return 'Tipe: Tidak diketahui';
}
}
List<Widget> _buildFillTheBlankPossibilities(String answer) {
List<String> possibilities = [
_capitalizeEachWord(answer),
answer.toLowerCase(),
_capitalizeFirstWordOnly(answer),
];
return possibilities.map((option) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
children: [
const Icon(Icons.arrow_right, size: 18, color: AppColors.softGrayText),
const SizedBox(width: 6),
Text(
option,
style: const TextStyle(color: AppColors.darkText),
),
],
),
);
}).toList();
}
String _capitalizeEachWord(String text) {
return text.split(' ').map((word) {
if (word.isEmpty) return word;
return word[0].toUpperCase() + word.substring(1).toLowerCase();
}).join(' ');
}
String _capitalizeFirstWordOnly(String text) {
if (text.isEmpty) return text;
List<String> parts = text.split(' ');
parts[0] = parts[0][0].toUpperCase() + parts[0].substring(1).toLowerCase();
for (int i = 1; i < parts.length; i++) {
parts[i] = parts[i].toLowerCase();
}
return parts.join(' ');
}
}

View File

@ -3,7 +3,6 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:quiz_app/app/routes/app_pages.dart'; import 'package:quiz_app/app/routes/app_pages.dart';
import 'package:quiz_app/core/utils/logger.dart';
import 'package:quiz_app/data/models/quiz/library_quiz_model.dart'; import 'package:quiz_app/data/models/quiz/library_quiz_model.dart';
import 'package:quiz_app/data/models/quiz/question_listings_model.dart'; import 'package:quiz_app/data/models/quiz/question_listings_model.dart';
@ -136,7 +135,7 @@ class QuizPlayController extends GetxController {
// ); // );
Get.toNamed( Get.toNamed(
AppRoutes.resultQuizPage, AppRoutes.resultQuizPage,
arguments: [quizData.questionListings, answeredQuestions], arguments: [quizData, answeredQuestions],
); );
} }

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:quiz_app/app/const/colors/app_colors.dart';
import 'package:quiz_app/app/const/enums/question_type.dart'; import 'package:quiz_app/app/const/enums/question_type.dart';
import 'package:quiz_app/app/routes/app_pages.dart'; import 'package:quiz_app/app/routes/app_pages.dart';
import 'package:quiz_app/core/utils/logger.dart'; import 'package:quiz_app/core/utils/logger.dart';
@ -106,153 +105,6 @@ class QuizPreviewController extends GetxController {
}).toList(); }).toList();
} }
Widget buildQuestionCard(QuestionData question) {
return Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 20),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.borderLight),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 6,
offset: const Offset(2, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Soal ${question.index}', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: AppColors.darkText)),
const SizedBox(height: 6),
Text(
_mapQuestionTypeToText(question.type),
style: const TextStyle(fontSize: 12, color: AppColors.softGrayText, fontStyle: FontStyle.italic),
),
const SizedBox(height: 12),
Text(
question.question ?? '-',
style: const TextStyle(fontSize: 16, color: AppColors.darkText),
),
const SizedBox(height: 16),
_buildAnswerSection(question),
const SizedBox(height: 10),
const Text(
'Durasi: 0 detik',
style: TextStyle(fontSize: 14, color: AppColors.softGrayText),
),
],
),
);
}
Widget _buildAnswerSection(QuestionData question) {
if (question.type == QuestionType.option && question.options != null) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: question.options!.map((option) {
bool isCorrect = question.correctAnswerIndex == option.index;
return Container(
margin: const EdgeInsets.symmetric(vertical: 4),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: BoxDecoration(
color: isCorrect ? AppColors.primaryBlue.withOpacity(0.1) : Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(
isCorrect ? Icons.check_circle_rounded : Icons.circle_outlined,
size: 18,
color: isCorrect ? AppColors.primaryBlue : AppColors.softGrayText,
),
const SizedBox(width: 8),
Expanded(
child: Text(
option.text,
style: TextStyle(
fontWeight: isCorrect ? FontWeight.bold : FontWeight.normal,
color: isCorrect ? AppColors.primaryBlue : AppColors.darkText,
),
),
),
],
),
);
}).toList(),
);
} else if (question.type == QuestionType.fillTheBlank) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildFillTheBlankPossibilities(question.answer ?? '-'),
);
} else if (question.type == QuestionType.trueOrFalse) {
return Text(
'Jawaban: ${question.answer ?? '-'}',
style: const TextStyle(color: AppColors.softGrayText),
);
} else {
return const SizedBox();
}
}
String _mapQuestionTypeToText(QuestionType? type) {
switch (type) {
case QuestionType.option:
return 'Tipe: Pilihan Ganda';
case QuestionType.fillTheBlank:
return 'Tipe: Isian Kosong';
case QuestionType.trueOrFalse:
return 'Tipe: Benar / Salah';
default:
return 'Tipe: Tidak diketahui';
}
}
List<Widget> _buildFillTheBlankPossibilities(String answer) {
List<String> possibilities = [
_capitalizeEachWord(answer),
answer.toLowerCase(),
_capitalizeFirstWordOnly(answer),
];
return possibilities.map((option) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
children: [
const Icon(Icons.arrow_right, size: 18, color: AppColors.softGrayText),
const SizedBox(width: 6),
Text(
option,
style: const TextStyle(color: AppColors.darkText),
),
],
),
);
}).toList();
}
String _capitalizeEachWord(String text) {
return text.split(' ').map((word) {
if (word.isEmpty) return word;
return word[0].toUpperCase() + word.substring(1).toLowerCase();
}).join(' ');
}
String _capitalizeFirstWordOnly(String text) {
if (text.isEmpty) return text;
List<String> parts = text.split(' ');
parts[0] = parts[0][0].toUpperCase() + parts[0].substring(1).toLowerCase();
for (int i = 1; i < parts.length; i++) {
parts[i] = parts[i].toLowerCase();
}
return parts.join(' ');
}
@override @override
void onClose() { void onClose() {
titleController.dispose(); titleController.dispose();

View File

@ -4,6 +4,7 @@ import 'package:quiz_app/app/const/colors/app_colors.dart';
import 'package:quiz_app/component/global_button.dart'; import 'package:quiz_app/component/global_button.dart';
import 'package:quiz_app/component/global_text_field.dart'; import 'package:quiz_app/component/global_text_field.dart';
import 'package:quiz_app/component/label_text_field.dart'; import 'package:quiz_app/component/label_text_field.dart';
import 'package:quiz_app/component/widget/question_container_widget.dart';
import 'package:quiz_app/feature/quiz_preview/controller/quiz_preview_controller.dart'; import 'package:quiz_app/feature/quiz_preview/controller/quiz_preview_controller.dart';
class QuizPreviewPage extends GetView<QuizPreviewController> { class QuizPreviewPage extends GetView<QuizPreviewController> {
@ -61,7 +62,7 @@ class QuizPreviewPage extends GetView<QuizPreviewController> {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: controller.data.map((question) { children: controller.data.map((question) {
return controller.buildQuestionCard(question); return QuestionContainerWidget(question: question);
}).toList(), }).toList(),
); );
} }

View File

@ -1,8 +1,12 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:quiz_app/app/const/enums/question_type.dart';
import 'package:quiz_app/data/models/quiz/library_quiz_model.dart';
import 'package:quiz_app/data/models/quiz/question_listings_model.dart'; import 'package:quiz_app/data/models/quiz/question_listings_model.dart';
import 'package:quiz_app/data/models/quiz/quiestion_data_model.dart';
import 'package:quiz_app/feature/quiz_play/controller/quiz_play_controller.dart'; import 'package:quiz_app/feature/quiz_play/controller/quiz_play_controller.dart';
class QuizResultController extends GetxController { class QuizResultController extends GetxController {
late final QuizData question;
late final List<QuestionListing> questions; late final List<QuestionListing> questions;
late final List<AnsweredQuestion> answers; late final List<AnsweredQuestion> answers;
@ -20,7 +24,8 @@ class QuizResultController extends GetxController {
void loadData() { void loadData() {
final args = Get.arguments as List<dynamic>; final args = Get.arguments as List<dynamic>;
questions = args[0] as List<QuestionListing>; question = args[0] as QuizData;
questions = question.questionListings;
answers = args[1] as List<AnsweredQuestion>; answers = args[1] as List<AnsweredQuestion>;
totalQuestions.value = questions.length; totalQuestions.value = questions.length;
} }
@ -40,4 +45,49 @@ class QuizResultController extends GetxController {
return "Belum Lulus 😔"; return "Belum Lulus 😔";
} }
} }
QuestionData mapQuestionListingToQuestionData(QuestionListing questionListing, int index) {
// Convert type string ke enum
QuestionType? questionType;
switch (questionListing.type) {
case 'fill_the_blank':
questionType = QuestionType.fillTheBlank;
break;
case 'option':
questionType = QuestionType.option;
break;
case 'true_false':
questionType = QuestionType.trueOrFalse;
break;
default:
questionType = null;
}
// Convert options ke OptionData
List<OptionData>? optionDataList;
if (questionListing.options != null) {
optionDataList = [];
for (int i = 0; i < questionListing.options!.length; i++) {
optionDataList.add(OptionData(index: i, text: questionListing.options![i]));
}
}
// Cari correctAnswerIndex kalau tipe-nya option
int? correctAnswerIndex;
if (questionType == QuestionType.option && optionDataList != null) {
correctAnswerIndex = optionDataList.indexWhere((option) => option.text == questionListing.targetAnswer);
if (correctAnswerIndex == -1) {
correctAnswerIndex = null; // Kalau tidak ketemu
}
}
return QuestionData(
index: index,
question: questionListing.question,
answer: questionListing.targetAnswer,
options: optionDataList,
correctAnswerIndex: correctAnswerIndex,
type: questionType,
);
}
} }

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.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/component/widget/question_container_widget.dart';
import 'package:quiz_app/feature/quiz_result/controller/quiz_result_controller.dart'; import 'package:quiz_app/feature/quiz_result/controller/quiz_result_controller.dart';
class QuizResultView extends GetView<QuizResultController> { class QuizResultView extends GetView<QuizResultController> {
@ -33,7 +34,9 @@ class QuizResultView extends GetView<QuizResultController> {
child: ListView.builder( child: ListView.builder(
itemCount: controller.questions.length, itemCount: controller.questions.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return _buildQuestionResult(index); return QuestionContainerWidget(
question: controller.mapQuestionListingToQuestionData(controller.questions[index], index),
answeredQuestion: controller.answers[index]);
}, },
), ),
), ),
@ -71,76 +74,76 @@ class QuizResultView extends GetView<QuizResultController> {
); );
} }
Widget _buildQuestionResult(int index) { // Widget _buildQuestionResult(int index) {
final answered = controller.answers[index]; // final answered = controller.answers[index];
final isCorrect = answered.isCorrect; // final isCorrect = answered.isCorrect;
return Container( // return Container(
width: double.infinity, // width: double.infinity,
margin: const EdgeInsets.only(bottom: 16), // margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(16), // padding: const EdgeInsets.all(16),
decoration: BoxDecoration( // decoration: BoxDecoration(
color: Colors.white, // color: Colors.white,
borderRadius: BorderRadius.circular(12), // borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.borderLight), // border: Border.all(color: AppColors.borderLight),
boxShadow: [ // boxShadow: [
BoxShadow( // BoxShadow(
color: Colors.black.withOpacity(0.05), // color: Colors.black.withOpacity(0.05),
blurRadius: 6, // blurRadius: 6,
offset: const Offset(2, 2), // offset: const Offset(2, 2),
), // ),
], // ],
), // ),
child: Column( // child: Column(
crossAxisAlignment: CrossAxisAlignment.start, // crossAxisAlignment: CrossAxisAlignment.start,
children: [ // children: [
Text('Soal ${answered.index + 1}', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: AppColors.darkText)), // Text('Soal ${answered.index + 1}', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: AppColors.darkText)),
const SizedBox(height: 6), // const SizedBox(height: 6),
Text( // Text(
// Tidak ada tipe soal di AnsweredQuestion, jadi kalau mau kasih tipe harus pakai question[index].type // // Tidak ada tipe soal di AnsweredQuestion, jadi kalau mau kasih tipe harus pakai question[index].type
// kalau tidak mau ribet, ini bisa dihapus saja // // kalau tidak mau ribet, ini bisa dihapus saja
// controller.mapQuestionTypeToText(controller.questions[answered.index].type), // // controller.mapQuestionTypeToText(controller.questions[answered.index].type),
'', // kosongkan dulu // '', // kosongkan dulu
style: const TextStyle(fontSize: 12, color: AppColors.softGrayText, fontStyle: FontStyle.italic), // style: const TextStyle(fontSize: 12, color: AppColors.softGrayText, fontStyle: FontStyle.italic),
), // ),
const SizedBox(height: 12), // const SizedBox(height: 12),
Text( // Text(
answered.question, // answered.question,
style: const TextStyle(fontSize: 16, color: AppColors.darkText), // style: const TextStyle(fontSize: 16, color: AppColors.darkText),
), // ),
const SizedBox(height: 12), // const SizedBox(height: 12),
Row( // Row(
crossAxisAlignment: CrossAxisAlignment.start, // crossAxisAlignment: CrossAxisAlignment.start,
children: [ // children: [
const Icon(Icons.person, size: 18, color: AppColors.primaryBlue), // const Icon(Icons.person, size: 18, color: AppColors.primaryBlue),
const SizedBox(width: 6), // const SizedBox(width: 6),
Expanded( // Expanded(
child: Text( // child: Text(
"Jawaban Kamu: ${answered.selectedAnswer}", // "Jawaban Kamu: ${answered.selectedAnswer}",
style: TextStyle( // style: TextStyle(
color: isCorrect ? Colors.green : Colors.red, // color: isCorrect ? Colors.green : Colors.red,
fontWeight: FontWeight.bold, // fontWeight: FontWeight.bold,
), // ),
), // ),
), // ),
], // ],
), // ),
const SizedBox(height: 4), // const SizedBox(height: 4),
Row( // Row(
crossAxisAlignment: CrossAxisAlignment.start, // crossAxisAlignment: CrossAxisAlignment.start,
children: [ // children: [
const Icon(Icons.check, size: 18, color: AppColors.softGrayText), // const Icon(Icons.check, size: 18, color: AppColors.softGrayText),
const SizedBox(width: 6), // const SizedBox(width: 6),
Expanded( // Expanded(
child: Text( // child: Text(
"Jawaban Benar: ${answered.correctAnswer}", // "Jawaban Benar: ${answered.correctAnswer}",
style: const TextStyle(color: AppColors.darkText), // style: const TextStyle(color: AppColors.darkText),
), // ),
), // ),
], // ],
), // ),
], // ],
), // ),
); // );
} // }
} }