feat: add index and input validation
This commit is contained in:
parent
51182b8c7b
commit
92f349e8ba
|
@ -49,8 +49,8 @@ class QuestionContainerWidget extends StatelessWidget {
|
||||||
_buildAnsweredSection(answeredQuestion!),
|
_buildAnsweredSection(answeredQuestion!),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
const Text(
|
Text(
|
||||||
'Durasi: 0 detik',
|
'Durasi: ${question.duration} detik',
|
||||||
style: TextStyle(fontSize: 14, color: AppColors.softGrayText),
|
style: TextStyle(fontSize: 14, color: AppColors.softGrayText),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
class QuestionListing {
|
class QuestionListing {
|
||||||
|
final int index;
|
||||||
final String question;
|
final String question;
|
||||||
final String targetAnswer;
|
final String targetAnswer;
|
||||||
final int duration;
|
final int duration;
|
||||||
|
@ -6,6 +7,7 @@ class QuestionListing {
|
||||||
final List<String>? options;
|
final List<String>? options;
|
||||||
|
|
||||||
QuestionListing({
|
QuestionListing({
|
||||||
|
required this.index,
|
||||||
required this.question,
|
required this.question,
|
||||||
required this.targetAnswer,
|
required this.targetAnswer,
|
||||||
required this.duration,
|
required this.duration,
|
||||||
|
@ -15,6 +17,7 @@ class QuestionListing {
|
||||||
|
|
||||||
factory QuestionListing.fromJson(Map<String, dynamic> json) {
|
factory QuestionListing.fromJson(Map<String, dynamic> json) {
|
||||||
return QuestionListing(
|
return QuestionListing(
|
||||||
|
index: json['index'],
|
||||||
question: json['question'],
|
question: json['question'],
|
||||||
targetAnswer: json['target_answer'],
|
targetAnswer: json['target_answer'],
|
||||||
duration: json['duration'],
|
duration: json['duration'],
|
||||||
|
@ -25,6 +28,7 @@ class QuestionListing {
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return {
|
return {
|
||||||
|
'index': index,
|
||||||
'question': question,
|
'question': question,
|
||||||
'target_answer': targetAnswer,
|
'target_answer': targetAnswer,
|
||||||
'duration': duration,
|
'duration': duration,
|
||||||
|
|
|
@ -14,6 +14,7 @@ class QuestionData {
|
||||||
final List<OptionData>? options;
|
final List<OptionData>? options;
|
||||||
final int? correctAnswerIndex;
|
final int? correctAnswerIndex;
|
||||||
final QuestionType? type;
|
final QuestionType? type;
|
||||||
|
final int duration;
|
||||||
|
|
||||||
QuestionData({
|
QuestionData({
|
||||||
required this.index,
|
required this.index,
|
||||||
|
@ -21,17 +22,11 @@ class QuestionData {
|
||||||
this.answer,
|
this.answer,
|
||||||
this.options,
|
this.options,
|
||||||
this.correctAnswerIndex,
|
this.correctAnswerIndex,
|
||||||
|
this.duration = 30,
|
||||||
this.type,
|
this.type,
|
||||||
});
|
});
|
||||||
|
|
||||||
QuestionData copyWith({
|
QuestionData copyWith({int? index, String? question, String? answer, List<OptionData>? options, int? correctAnswerIndex, QuestionType? type, int? duration}) {
|
||||||
int? index,
|
|
||||||
String? question,
|
|
||||||
String? answer,
|
|
||||||
List<OptionData>? options,
|
|
||||||
int? correctAnswerIndex,
|
|
||||||
QuestionType? type,
|
|
||||||
}) {
|
|
||||||
return QuestionData(
|
return QuestionData(
|
||||||
index: index ?? this.index,
|
index: index ?? this.index,
|
||||||
question: question ?? this.question,
|
question: question ?? this.question,
|
||||||
|
@ -39,6 +34,7 @@ class QuestionData {
|
||||||
options: options ?? this.options,
|
options: options ?? this.options,
|
||||||
correctAnswerIndex: correctAnswerIndex ?? this.correctAnswerIndex,
|
correctAnswerIndex: correctAnswerIndex ?? this.correctAnswerIndex,
|
||||||
type: type ?? this.type,
|
type: type ?? this.type,
|
||||||
|
duration: duration ?? this.duration,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,8 @@ class QuizCreationController extends GetxController {
|
||||||
RxList<QuestionData> quizData = <QuestionData>[QuestionData(index: 1, type: QuestionType.fillTheBlank)].obs;
|
RxList<QuestionData> quizData = <QuestionData>[QuestionData(index: 1, type: QuestionType.fillTheBlank)].obs;
|
||||||
RxInt selectedQuizIndex = 0.obs;
|
RxInt selectedQuizIndex = 0.obs;
|
||||||
|
|
||||||
|
RxInt currentDuration = 30.obs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
|
@ -91,6 +93,8 @@ class QuizCreationController extends GetxController {
|
||||||
questionTC.text = data.question ?? "";
|
questionTC.text = data.question ?? "";
|
||||||
answerTC.text = data.answer ?? "";
|
answerTC.text = data.answer ?? "";
|
||||||
|
|
||||||
|
currentDuration.value = data.duration;
|
||||||
|
|
||||||
currentQuestionType.value = data.type ?? QuestionType.fillTheBlank;
|
currentQuestionType.value = data.type ?? QuestionType.fillTheBlank;
|
||||||
if (currentQuestionType.value == QuestionType.option) {
|
if (currentQuestionType.value == QuestionType.option) {
|
||||||
for (int i = 0; i < optionTCList.length; i++) {
|
for (int i = 0; i < optionTCList.length; i++) {
|
||||||
|
@ -118,13 +122,7 @@ class QuizCreationController extends GetxController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateCurrentQuestion({
|
void _updateCurrentQuestion({String? question, String? answer, List<OptionData>? options, int? correctAnswerIndex, QuestionType? type, int? duration}) {
|
||||||
String? question,
|
|
||||||
String? answer,
|
|
||||||
List<OptionData>? options,
|
|
||||||
int? correctAnswerIndex,
|
|
||||||
QuestionType? type,
|
|
||||||
}) {
|
|
||||||
final current = quizData[selectedQuizIndex.value];
|
final current = quizData[selectedQuizIndex.value];
|
||||||
quizData[selectedQuizIndex.value] = current.copyWith(
|
quizData[selectedQuizIndex.value] = current.copyWith(
|
||||||
question: question,
|
question: question,
|
||||||
|
@ -132,22 +130,55 @@ class QuizCreationController extends GetxController {
|
||||||
options: options,
|
options: options,
|
||||||
correctAnswerIndex: correctAnswerIndex,
|
correctAnswerIndex: correctAnswerIndex,
|
||||||
type: type,
|
type: type,
|
||||||
|
duration: duration,
|
||||||
);
|
);
|
||||||
|
|
||||||
// ??
|
|
||||||
// (currentQuestionType.value == QuestionType.option
|
|
||||||
// ? List.generate(
|
|
||||||
// optionTCList.length,
|
|
||||||
// (index) => OptionData(index: index, text: optionTCList[index].text),
|
|
||||||
// )
|
|
||||||
// : null),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateTOFAnswer(bool answer) {
|
void updateTOFAnswer(bool answer) {
|
||||||
_updateCurrentQuestion(answer: answer.toString());
|
_updateCurrentQuestion(answer: answer.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onDurationChange(int? duration) {
|
||||||
|
currentDuration.value = duration ?? 30;
|
||||||
|
_updateCurrentQuestion(duration: duration);
|
||||||
|
}
|
||||||
|
|
||||||
void onDone() {
|
void onDone() {
|
||||||
|
for (final value in quizData) {
|
||||||
|
if (value.question == null) {
|
||||||
|
Get.snackbar(
|
||||||
|
'Field kosong di soal ${value.index}',
|
||||||
|
'Hapus jika tidak digunakan',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.type == QuestionType.option) {
|
||||||
|
if (value.correctAnswerIndex == null) {
|
||||||
|
Get.snackbar(
|
||||||
|
'Field kosong di soal ${value.index}',
|
||||||
|
'Hapus jika tidak digunakan',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value.options == null || value.options!.length < 4) {
|
||||||
|
Get.snackbar(
|
||||||
|
'Pilihan jawaban kurang dari 4 di soal ${value.index}',
|
||||||
|
'Tambahkan pilihan jawaban',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (value.answer == null) {
|
||||||
|
Get.snackbar(
|
||||||
|
'Field kosong di soal ${value.index}',
|
||||||
|
'Hapus jika tidak digunakan',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Get.toNamed(AppRoutes.quizPreviewPage, arguments: quizData);
|
Get.toNamed(AppRoutes.quizPreviewPage, arguments: quizData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -180,20 +180,20 @@ class CustomQuestionComponent extends GetView<QuizCreationController> {
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(color: AppColors.borderLight),
|
border: Border.all(color: AppColors.borderLight),
|
||||||
),
|
),
|
||||||
child: DropdownButtonFormField<String>(
|
child: Obx(() => DropdownButtonFormField<int>(
|
||||||
value: '1 minute',
|
value: controller.currentDuration.value,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
contentPadding: EdgeInsets.symmetric(vertical: 14),
|
contentPadding: EdgeInsets.symmetric(vertical: 14),
|
||||||
),
|
),
|
||||||
style: const TextStyle(color: AppColors.darkText, fontWeight: FontWeight.w500),
|
style: const TextStyle(color: AppColors.darkText, fontWeight: FontWeight.w500),
|
||||||
items: const [
|
items: const [
|
||||||
DropdownMenuItem(value: '30 seconds', child: Text('30 seconds')),
|
DropdownMenuItem(value: 10, child: Text('10 detik')),
|
||||||
DropdownMenuItem(value: '1 minute', child: Text('1 minute')),
|
DropdownMenuItem(value: 20, child: Text('20 detik')),
|
||||||
DropdownMenuItem(value: '2 minutes', child: Text('2 minutes')),
|
DropdownMenuItem(value: 30, child: Text('30 detik')),
|
||||||
|
DropdownMenuItem(value: 60, child: Text('1 menit')),
|
||||||
],
|
],
|
||||||
onChanged: (value) {},
|
onChanged: controller.onDurationChange)),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -123,17 +123,8 @@ class QuizPlayController extends GetxController {
|
||||||
|
|
||||||
void _finishQuiz() {
|
void _finishQuiz() {
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
// logC.i(answeredQuestions.map((e) => e.toJson()).toList());
|
|
||||||
// Get.defaultDialog(
|
Get.offAllNamed(
|
||||||
// title: "Selesai",
|
|
||||||
// middleText: "Kamu telah menyelesaikan semua soal!",
|
|
||||||
// onConfirm: () {
|
|
||||||
// Get.back();
|
|
||||||
// Get.back();
|
|
||||||
// },
|
|
||||||
// textConfirm: "OK",
|
|
||||||
// );
|
|
||||||
Get.toNamed(
|
|
||||||
AppRoutes.resultQuizPage,
|
AppRoutes.resultQuizPage,
|
||||||
arguments: [quizData, answeredQuestions],
|
arguments: [quizData, answeredQuestions],
|
||||||
);
|
);
|
||||||
|
|
|
@ -72,10 +72,14 @@ class QuizPreviewController extends GetxController {
|
||||||
}
|
}
|
||||||
|
|
||||||
List<QuestionListing> _mapQuestionsToListings(List<QuestionData> questions) {
|
List<QuestionListing> _mapQuestionsToListings(List<QuestionData> questions) {
|
||||||
return questions.map((q) {
|
return questions.asMap().entries.map((entry) {
|
||||||
|
final index = entry.key;
|
||||||
|
final q = entry.value;
|
||||||
|
|
||||||
String typeString;
|
String typeString;
|
||||||
String answer = "";
|
String answer = "";
|
||||||
List<String>? option;
|
List<String>? option;
|
||||||
|
|
||||||
switch (q.type) {
|
switch (q.type) {
|
||||||
case QuestionType.fillTheBlank:
|
case QuestionType.fillTheBlank:
|
||||||
typeString = 'fill_the_blank';
|
typeString = 'fill_the_blank';
|
||||||
|
@ -96,9 +100,10 @@ class QuizPreviewController extends GetxController {
|
||||||
}
|
}
|
||||||
|
|
||||||
return QuestionListing(
|
return QuestionListing(
|
||||||
|
index: index,
|
||||||
question: q.question ?? '',
|
question: q.question ?? '',
|
||||||
targetAnswer: answer,
|
targetAnswer: answer,
|
||||||
duration: 30,
|
duration: q.duration,
|
||||||
type: typeString,
|
type: typeString,
|
||||||
options: option,
|
options: option,
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.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/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';
|
||||||
import 'package:quiz_app/data/models/quiz/quiestion_data_model.dart';
|
import 'package:quiz_app/data/models/quiz/quiestion_data_model.dart';
|
||||||
|
@ -39,11 +40,7 @@ class QuizResultController extends GetxController {
|
||||||
}
|
}
|
||||||
|
|
||||||
String getResultMessage() {
|
String getResultMessage() {
|
||||||
if (scorePercentage.value >= 80) {
|
return "Nilai kamu ${scorePercentage.value}";
|
||||||
return "Lulus 🎉";
|
|
||||||
} else {
|
|
||||||
return "Belum Lulus 😔";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QuestionData mapQuestionListingToQuestionData(QuestionListing questionListing, int index) {
|
QuestionData mapQuestionListingToQuestionData(QuestionListing questionListing, int index) {
|
||||||
|
@ -90,4 +87,8 @@ class QuizResultController extends GetxController {
|
||||||
type: questionType,
|
type: questionType,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onPopInvoke(bool isPop, dynamic value) {
|
||||||
|
Get.offNamed(AppRoutes.mainPage, arguments: 3);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,25 +9,19 @@ class QuizResultView extends GetView<QuizResultController> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return PopScope(
|
||||||
|
canPop: false,
|
||||||
|
onPopInvokedWithResult: controller.onPopInvoke,
|
||||||
|
child: Scaffold(
|
||||||
backgroundColor: const Color(0xFFF9FAFB),
|
backgroundColor: const Color(0xFFF9FAFB),
|
||||||
appBar: AppBar(
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
elevation: 0,
|
|
||||||
automaticallyImplyLeading: false,
|
|
||||||
title: const Text(
|
|
||||||
'Hasil Kuis',
|
|
||||||
style: TextStyle(color: Colors.black, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
iconTheme: const IconThemeData(color: Colors.black),
|
|
||||||
centerTitle: true,
|
|
||||||
),
|
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Obx(() => Column(
|
child: Obx(() => Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
_buildCustomAppBar(context),
|
||||||
|
const SizedBox(height: 16),
|
||||||
_buildScoreSummary(),
|
_buildScoreSummary(),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
|
@ -36,7 +30,8 @@ class QuizResultView extends GetView<QuizResultController> {
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return QuestionContainerWidget(
|
return QuestionContainerWidget(
|
||||||
question: controller.mapQuestionListingToQuestionData(controller.questions[index], index),
|
question: controller.mapQuestionListingToQuestionData(controller.questions[index], index),
|
||||||
answeredQuestion: controller.answers[index]);
|
answeredQuestion: controller.answers[index],
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -44,6 +39,25 @@ class QuizResultView extends GetView<QuizResultController> {
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCustomAppBar(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back, color: Colors.black),
|
||||||
|
onPressed: () {
|
||||||
|
controller.onPopInvoke(true, null);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
const Text(
|
||||||
|
'Hasil Kuis',
|
||||||
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,77 +87,4 @@ class QuizResultView extends GetView<QuizResultController> {
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Widget _buildQuestionResult(int index) {
|
|
||||||
// final answered = controller.answers[index];
|
|
||||||
// final isCorrect = answered.isCorrect;
|
|
||||||
|
|
||||||
// return Container(
|
|
||||||
// width: double.infinity,
|
|
||||||
// margin: const EdgeInsets.only(bottom: 16),
|
|
||||||
// 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 ${answered.index + 1}', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: AppColors.darkText)),
|
|
||||||
// const SizedBox(height: 6),
|
|
||||||
// Text(
|
|
||||||
// // Tidak ada tipe soal di AnsweredQuestion, jadi kalau mau kasih tipe harus pakai question[index].type
|
|
||||||
// // kalau tidak mau ribet, ini bisa dihapus saja
|
|
||||||
// // controller.mapQuestionTypeToText(controller.questions[answered.index].type),
|
|
||||||
// '', // kosongkan dulu
|
|
||||||
// style: const TextStyle(fontSize: 12, color: AppColors.softGrayText, fontStyle: FontStyle.italic),
|
|
||||||
// ),
|
|
||||||
// const SizedBox(height: 12),
|
|
||||||
// Text(
|
|
||||||
// answered.question,
|
|
||||||
// style: const TextStyle(fontSize: 16, color: AppColors.darkText),
|
|
||||||
// ),
|
|
||||||
// const SizedBox(height: 12),
|
|
||||||
// Row(
|
|
||||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
// children: [
|
|
||||||
// const Icon(Icons.person, size: 18, color: AppColors.primaryBlue),
|
|
||||||
// const SizedBox(width: 6),
|
|
||||||
// Expanded(
|
|
||||||
// child: Text(
|
|
||||||
// "Jawaban Kamu: ${answered.selectedAnswer}",
|
|
||||||
// style: TextStyle(
|
|
||||||
// color: isCorrect ? Colors.green : Colors.red,
|
|
||||||
// fontWeight: FontWeight.bold,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
// const SizedBox(height: 4),
|
|
||||||
// Row(
|
|
||||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
// children: [
|
|
||||||
// const Icon(Icons.check, size: 18, color: AppColors.softGrayText),
|
|
||||||
// const SizedBox(width: 6),
|
|
||||||
// Expanded(
|
|
||||||
// child: Text(
|
|
||||||
// "Jawaban Benar: ${answered.correctAnswer}",
|
|
||||||
// style: const TextStyle(color: AppColors.darkText),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue