feat: add index and input validation

This commit is contained in:
akhdanre 2025-04-29 23:00:48 +07:00
parent 51182b8c7b
commit 92f349e8ba
9 changed files with 130 additions and 161 deletions

View File

@ -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),
),
],

View File

@ -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,

View File

@ -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,
);
}
}

View File

@ -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);
}

View File

@ -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)),
);
}
}

View File

@ -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],
);

View File

@ -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,
);

View File

@ -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);
}
}

View File

@ -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),
// ),
// ),
// ],
// ),
// ],
// ),
// );
// }
}