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!),
|
||||
],
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
'Durasi: 0 detik',
|
||||
Text(
|
||||
'Durasi: ${question.duration} detik',
|
||||
style: TextStyle(fontSize: 14, color: AppColors.softGrayText),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
class QuestionListing {
|
||||
final int index;
|
||||
final String question;
|
||||
final String targetAnswer;
|
||||
final int duration;
|
||||
|
@ -6,6 +7,7 @@ class QuestionListing {
|
|||
final List<String>? options;
|
||||
|
||||
QuestionListing({
|
||||
required this.index,
|
||||
required this.question,
|
||||
required this.targetAnswer,
|
||||
required this.duration,
|
||||
|
@ -15,6 +17,7 @@ class QuestionListing {
|
|||
|
||||
factory QuestionListing.fromJson(Map<String, dynamic> json) {
|
||||
return QuestionListing(
|
||||
index: json['index'],
|
||||
question: json['question'],
|
||||
targetAnswer: json['target_answer'],
|
||||
duration: json['duration'],
|
||||
|
@ -25,6 +28,7 @@ class QuestionListing {
|
|||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'index': index,
|
||||
'question': question,
|
||||
'target_answer': targetAnswer,
|
||||
'duration': duration,
|
||||
|
|
|
@ -14,6 +14,7 @@ class QuestionData {
|
|||
final List<OptionData>? options;
|
||||
final int? correctAnswerIndex;
|
||||
final QuestionType? type;
|
||||
final int duration;
|
||||
|
||||
QuestionData({
|
||||
required this.index,
|
||||
|
@ -21,17 +22,11 @@ class QuestionData {
|
|||
this.answer,
|
||||
this.options,
|
||||
this.correctAnswerIndex,
|
||||
this.duration = 30,
|
||||
this.type,
|
||||
});
|
||||
|
||||
QuestionData copyWith({
|
||||
int? index,
|
||||
String? question,
|
||||
String? answer,
|
||||
List<OptionData>? options,
|
||||
int? correctAnswerIndex,
|
||||
QuestionType? type,
|
||||
}) {
|
||||
QuestionData copyWith({int? index, String? question, String? answer, List<OptionData>? options, int? correctAnswerIndex, QuestionType? type, int? duration}) {
|
||||
return QuestionData(
|
||||
index: index ?? this.index,
|
||||
question: question ?? this.question,
|
||||
|
@ -39,6 +34,7 @@ class QuestionData {
|
|||
options: options ?? this.options,
|
||||
correctAnswerIndex: correctAnswerIndex ?? this.correctAnswerIndex,
|
||||
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;
|
||||
RxInt selectedQuizIndex = 0.obs;
|
||||
|
||||
RxInt currentDuration = 30.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
@ -91,6 +93,8 @@ class QuizCreationController extends GetxController {
|
|||
questionTC.text = data.question ?? "";
|
||||
answerTC.text = data.answer ?? "";
|
||||
|
||||
currentDuration.value = data.duration;
|
||||
|
||||
currentQuestionType.value = data.type ?? QuestionType.fillTheBlank;
|
||||
if (currentQuestionType.value == QuestionType.option) {
|
||||
for (int i = 0; i < optionTCList.length; i++) {
|
||||
|
@ -118,13 +122,7 @@ class QuizCreationController extends GetxController {
|
|||
}
|
||||
}
|
||||
|
||||
void _updateCurrentQuestion({
|
||||
String? question,
|
||||
String? answer,
|
||||
List<OptionData>? options,
|
||||
int? correctAnswerIndex,
|
||||
QuestionType? type,
|
||||
}) {
|
||||
void _updateCurrentQuestion({String? question, String? answer, List<OptionData>? options, int? correctAnswerIndex, QuestionType? type, int? duration}) {
|
||||
final current = quizData[selectedQuizIndex.value];
|
||||
quizData[selectedQuizIndex.value] = current.copyWith(
|
||||
question: question,
|
||||
|
@ -132,22 +130,55 @@ class QuizCreationController extends GetxController {
|
|||
options: options,
|
||||
correctAnswerIndex: correctAnswerIndex,
|
||||
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) {
|
||||
_updateCurrentQuestion(answer: answer.toString());
|
||||
}
|
||||
|
||||
void onDurationChange(int? duration) {
|
||||
currentDuration.value = duration ?? 30;
|
||||
_updateCurrentQuestion(duration: duration);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -180,20 +180,20 @@ class CustomQuestionComponent extends GetView<QuizCreationController> {
|
|||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColors.borderLight),
|
||||
),
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: '1 minute',
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 14),
|
||||
),
|
||||
style: const TextStyle(color: AppColors.darkText, fontWeight: FontWeight.w500),
|
||||
items: const [
|
||||
DropdownMenuItem(value: '30 seconds', child: Text('30 seconds')),
|
||||
DropdownMenuItem(value: '1 minute', child: Text('1 minute')),
|
||||
DropdownMenuItem(value: '2 minutes', child: Text('2 minutes')),
|
||||
],
|
||||
onChanged: (value) {},
|
||||
),
|
||||
child: Obx(() => DropdownButtonFormField<int>(
|
||||
value: controller.currentDuration.value,
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 14),
|
||||
),
|
||||
style: const TextStyle(color: AppColors.darkText, fontWeight: FontWeight.w500),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 10, child: Text('10 detik')),
|
||||
DropdownMenuItem(value: 20, child: Text('20 detik')),
|
||||
DropdownMenuItem(value: 30, child: Text('30 detik')),
|
||||
DropdownMenuItem(value: 60, child: Text('1 menit')),
|
||||
],
|
||||
onChanged: controller.onDurationChange)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -123,17 +123,8 @@ class QuizPlayController extends GetxController {
|
|||
|
||||
void _finishQuiz() {
|
||||
_timer?.cancel();
|
||||
// logC.i(answeredQuestions.map((e) => e.toJson()).toList());
|
||||
// Get.defaultDialog(
|
||||
// title: "Selesai",
|
||||
// middleText: "Kamu telah menyelesaikan semua soal!",
|
||||
// onConfirm: () {
|
||||
// Get.back();
|
||||
// Get.back();
|
||||
// },
|
||||
// textConfirm: "OK",
|
||||
// );
|
||||
Get.toNamed(
|
||||
|
||||
Get.offAllNamed(
|
||||
AppRoutes.resultQuizPage,
|
||||
arguments: [quizData, answeredQuestions],
|
||||
);
|
||||
|
|
|
@ -72,10 +72,14 @@ class QuizPreviewController extends GetxController {
|
|||
}
|
||||
|
||||
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 answer = "";
|
||||
List<String>? option;
|
||||
|
||||
switch (q.type) {
|
||||
case QuestionType.fillTheBlank:
|
||||
typeString = 'fill_the_blank';
|
||||
|
@ -96,9 +100,10 @@ class QuizPreviewController extends GetxController {
|
|||
}
|
||||
|
||||
return QuestionListing(
|
||||
index: index,
|
||||
question: q.question ?? '',
|
||||
targetAnswer: answer,
|
||||
duration: 30,
|
||||
duration: q.duration,
|
||||
type: typeString,
|
||||
options: option,
|
||||
);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:get/get.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/question_listings_model.dart';
|
||||
import 'package:quiz_app/data/models/quiz/quiestion_data_model.dart';
|
||||
|
@ -39,11 +40,7 @@ class QuizResultController extends GetxController {
|
|||
}
|
||||
|
||||
String getResultMessage() {
|
||||
if (scorePercentage.value >= 80) {
|
||||
return "Lulus 🎉";
|
||||
} else {
|
||||
return "Belum Lulus 😔";
|
||||
}
|
||||
return "Nilai kamu ${scorePercentage.value}";
|
||||
}
|
||||
|
||||
QuestionData mapQuestionListingToQuestionData(QuestionListing questionListing, int index) {
|
||||
|
@ -90,4 +87,8 @@ class QuizResultController extends GetxController {
|
|||
type: questionType,
|
||||
);
|
||||
}
|
||||
|
||||
void onPopInvoke(bool isPop, dynamic value) {
|
||||
Get.offNamed(AppRoutes.mainPage, arguments: 3);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,44 +9,58 @@ class QuizResultView extends GetView<QuizResultController> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
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(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Obx(() => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildScoreSummary(),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: controller.questions.length,
|
||||
itemBuilder: (context, index) {
|
||||
return QuestionContainerWidget(
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: controller.onPopInvoke,
|
||||
child: Scaffold(
|
||||
backgroundColor: const Color(0xFFF9FAFB),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Obx(() => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildCustomAppBar(context),
|
||||
const SizedBox(height: 16),
|
||||
_buildScoreSummary(),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: controller.questions.length,
|
||||
itemBuilder: (context, index) {
|
||||
return QuestionContainerWidget(
|
||||
question: controller.mapQuestionListingToQuestionData(controller.questions[index], index),
|
||||
answeredQuestion: controller.answers[index]);
|
||||
},
|
||||
answeredQuestion: controller.answers[index],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
],
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildScoreSummary() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
@ -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