develop #1
|
@ -0,0 +1 @@
|
|||
enum QuestionType { fillTheBlank, option, trueOrFalse }
|
|
@ -0,0 +1,43 @@
|
|||
import 'package:quiz_app/app/const/enums/question_type.dart';
|
||||
|
||||
class OptionData {
|
||||
final int index;
|
||||
final String text;
|
||||
|
||||
OptionData({required this.index, required this.text});
|
||||
}
|
||||
|
||||
class QuestionData {
|
||||
final int index;
|
||||
final String? question;
|
||||
final String? answer;
|
||||
final List<OptionData>? options;
|
||||
final int? correctAnswerIndex;
|
||||
final QuestionType? type;
|
||||
|
||||
QuestionData({
|
||||
required this.index,
|
||||
this.question,
|
||||
this.answer,
|
||||
this.options,
|
||||
this.correctAnswerIndex,
|
||||
this.type,
|
||||
});
|
||||
|
||||
QuestionData copyWith({
|
||||
String? question,
|
||||
String? answer,
|
||||
List<OptionData>? options,
|
||||
int? correctAnswerIndex,
|
||||
QuestionType? type,
|
||||
}) {
|
||||
return QuestionData(
|
||||
index: index,
|
||||
question: question ?? this.question,
|
||||
answer: answer ?? this.answer,
|
||||
options: options ?? this.options,
|
||||
correctAnswerIndex: correctAnswerIndex ?? this.correctAnswerIndex,
|
||||
type: type ?? this.type,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,17 +1,126 @@
|
|||
import 'package:flutter/widgets.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:quiz_app/app/const/enums/question_type.dart';
|
||||
import 'package:quiz_app/data/models/quiz/quiestion_data_model.dart';
|
||||
|
||||
class QuizCreationController extends GetxController {
|
||||
TextEditingController questionTC = TextEditingController();
|
||||
TextEditingController answerTC = TextEditingController();
|
||||
final TextEditingController questionTC = TextEditingController();
|
||||
final TextEditingController answerTC = TextEditingController();
|
||||
final List<TextEditingController> optionTCList = List.generate(4, (_) => TextEditingController());
|
||||
final RxInt selectedOptionIndex = 0.obs;
|
||||
|
||||
RxBool isGenerate = true.obs;
|
||||
|
||||
Rx<QuestionType> currentQuestionType = QuestionType.fillTheBlank.obs;
|
||||
RxList<QuestionData> quizData = <QuestionData>[QuestionData(index: 1)].obs;
|
||||
RxInt selectedQuizIndex = 0.obs;
|
||||
|
||||
onCreationTypeChange(bool value) => isGenerate.value = value;
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_initializeListeners();
|
||||
}
|
||||
|
||||
onQuestionTypeChange(QuestionType type) => currentQuestionType.value = type;
|
||||
void _initializeListeners() {
|
||||
// Listener untuk pertanyaan
|
||||
questionTC.addListener(() {
|
||||
if (quizData.isNotEmpty) {
|
||||
_updateCurrentQuestion(question: questionTC.text);
|
||||
}
|
||||
});
|
||||
|
||||
// Listener untuk jawaban langsung (Fill the Blank atau True/False)
|
||||
answerTC.addListener(() {
|
||||
if (quizData.isNotEmpty && currentQuestionType.value != QuestionType.option) {
|
||||
_updateCurrentQuestion(answer: answerTC.text);
|
||||
}
|
||||
});
|
||||
|
||||
// Listener untuk masing-masing pilihan opsi
|
||||
for (var i = 0; i < optionTCList.length; i++) {
|
||||
optionTCList[i].addListener(() {
|
||||
if (quizData.isNotEmpty && currentQuestionType.value == QuestionType.option) {
|
||||
_updateCurrentQuestion(
|
||||
options: List.generate(
|
||||
optionTCList.length,
|
||||
(index) => OptionData(index: index, text: optionTCList[index].text),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Listener perubahan tipe soal
|
||||
ever<QuestionType>(currentQuestionType, (type) {
|
||||
if (quizData.isNotEmpty) {
|
||||
_updateCurrentQuestion(type: type);
|
||||
}
|
||||
});
|
||||
|
||||
// Listener perubahan jawaban benar (untuk pilihan ganda)
|
||||
ever<int>(selectedOptionIndex, (index) {
|
||||
if (quizData.isNotEmpty && currentQuestionType.value == QuestionType.option) {
|
||||
_updateCurrentQuestion(correctAnswerIndex: index);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void onCreationTypeChange(bool value) {
|
||||
isGenerate.value = value;
|
||||
}
|
||||
|
||||
void onQuestionTypeChange(QuestionType type) {
|
||||
currentQuestionType.value = type;
|
||||
}
|
||||
|
||||
void onQuestionAdd() {
|
||||
quizData.add(QuestionData(index: quizData.length + 1));
|
||||
}
|
||||
|
||||
void onSelectedQuizItem(int index) {
|
||||
selectedQuizIndex.value = index;
|
||||
final data = quizData[index];
|
||||
|
||||
questionTC.text = data.question ?? "";
|
||||
answerTC.text = data.answer ?? "";
|
||||
currentQuestionType.value = data.type ?? QuestionType.fillTheBlank;
|
||||
|
||||
if (data.options != null && data.options!.isNotEmpty) {
|
||||
for (int i = 0; i < optionTCList.length; i++) {
|
||||
optionTCList[i].text = data.options!.length > i ? data.options![i].text : '';
|
||||
}
|
||||
selectedOptionIndex.value = data.correctAnswerIndex ?? 0;
|
||||
} else {
|
||||
for (var controller in optionTCList) {
|
||||
controller.clear();
|
||||
}
|
||||
selectedOptionIndex.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void _updateCurrentQuestion({
|
||||
String? question,
|
||||
String? answer,
|
||||
List<OptionData>? options,
|
||||
int? correctAnswerIndex,
|
||||
QuestionType? type,
|
||||
}) {
|
||||
final current = quizData[selectedQuizIndex.value];
|
||||
quizData[selectedQuizIndex.value] = current.copyWith(
|
||||
question: question,
|
||||
answer: answer,
|
||||
options: options ??
|
||||
(currentQuestionType.value == QuestionType.option
|
||||
? List.generate(
|
||||
optionTCList.length,
|
||||
(index) => OptionData(index: index, text: optionTCList[index].text),
|
||||
)
|
||||
: null),
|
||||
correctAnswerIndex: correctAnswerIndex,
|
||||
type: type,
|
||||
);
|
||||
}
|
||||
|
||||
void updateTOFAnswer(bool answer) {
|
||||
_updateCurrentQuestion(answer: answer.toString());
|
||||
}
|
||||
}
|
||||
|
||||
enum QuestionType { fillTheBlank, option, trueOrFalse }
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/material.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/feature/quiz_creation/controller/quiz_creation_controller.dart';
|
||||
import 'package:quiz_app/feature/quiz_creation/view/component/fill_the_blank_component.dart';
|
||||
import 'package:quiz_app/feature/quiz_creation/view/component/option_question_component.dart';
|
||||
|
@ -33,10 +34,7 @@ class CustomQuestionComponent extends GetView<QuizCreationController> {
|
|||
answerTC: controller.answerTC,
|
||||
);
|
||||
case QuestionType.option:
|
||||
return OptionQuestionComponent(
|
||||
questionTC: TextEditingController(),
|
||||
optionTCList: List.generate(4, (index) => TextEditingController()),
|
||||
);
|
||||
return OptionQuestionComponent();
|
||||
case QuestionType.trueOrFalse:
|
||||
return TrueFalseQuestionComponent(questionTC: controller.questionTC);
|
||||
}
|
||||
|
@ -44,35 +42,66 @@ class CustomQuestionComponent extends GetView<QuizCreationController> {
|
|||
}
|
||||
|
||||
Widget _buildNumberPicker() {
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: List.generate(14, (index) {
|
||||
return Container(
|
||||
width: 42,
|
||||
height: 42,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppColors.borderLight),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(2, 2),
|
||||
),
|
||||
],
|
||||
return Obx(
|
||||
() => SizedBox(
|
||||
height: 100,
|
||||
child: GridView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
childAspectRatio: 1,
|
||||
),
|
||||
child: Text(
|
||||
'${index + 1}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.darkText,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
itemCount: controller.quizData.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
final isLast = index == controller.quizData.length;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (isLast) {
|
||||
controller.onQuestionAdd();
|
||||
} else {
|
||||
controller.onSelectedQuizItem(index);
|
||||
}
|
||||
},
|
||||
child: Obx(() {
|
||||
bool isSelected = controller.selectedQuizIndex.value == index;
|
||||
return Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppColors.primaryBlue : Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: isSelected ? AppColors.primaryBlue : AppColors.borderLight,
|
||||
width: 2,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withAlpha(13),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(2, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: isLast
|
||||
? const Icon(Icons.add, color: AppColors.darkText)
|
||||
: Text(
|
||||
'${controller.quizData[index].index}',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
color: isSelected ? Colors.white : AppColors.darkText,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,23 +1,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:quiz_app/component/global_text_field.dart';
|
||||
import 'package:quiz_app/component/label_text_field.dart';
|
||||
import 'package:quiz_app/feature/quiz_creation/controller/quiz_creation_controller.dart'; // Pastikan import controllermu
|
||||
|
||||
class OptionQuestionComponent extends StatefulWidget {
|
||||
final TextEditingController questionTC;
|
||||
final List<TextEditingController> optionTCList;
|
||||
|
||||
const OptionQuestionComponent({
|
||||
super.key,
|
||||
required this.questionTC,
|
||||
required this.optionTCList,
|
||||
});
|
||||
|
||||
@override
|
||||
State<OptionQuestionComponent> createState() => _OptionQuestionComponentState();
|
||||
}
|
||||
|
||||
class _OptionQuestionComponentState extends State<OptionQuestionComponent> {
|
||||
String? selectedCorrectAnswer; // A, B, C, D
|
||||
class OptionQuestionComponent extends GetView<QuizCreationController> {
|
||||
const OptionQuestionComponent({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -26,19 +14,19 @@ class _OptionQuestionComponentState extends State<OptionQuestionComponent> {
|
|||
// Pertanyaan
|
||||
LabelTextField(label: "Pertanyaan"),
|
||||
GlobalTextField(
|
||||
controller: widget.questionTC,
|
||||
controller: controller.questionTC,
|
||||
limitTextLine: 3,
|
||||
hintText: "Tulis Pertanyaan",
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// Pilihan A, B, C, D
|
||||
...List.generate(widget.optionTCList.length, (index) {
|
||||
...List.generate(controller.optionTCList.length, (index) {
|
||||
return Column(
|
||||
children: [
|
||||
LabelTextField(label: "Pilihan ${String.fromCharCode(65 + index)}"),
|
||||
GlobalTextField(
|
||||
controller: widget.optionTCList[index],
|
||||
controller: controller.optionTCList[index],
|
||||
hintText: "Tulis Pilihan ${String.fromCharCode(65 + index)}",
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
@ -46,8 +34,9 @@ class _OptionQuestionComponentState extends State<OptionQuestionComponent> {
|
|||
);
|
||||
}),
|
||||
|
||||
// Jawaban Benar Dropdown
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Jawaban Benar Dropdown
|
||||
LabelTextField(label: "Jawaban Benar"),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
|
@ -56,23 +45,24 @@ class _OptionQuestionComponentState extends State<OptionQuestionComponent> {
|
|||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
value: selectedCorrectAnswer,
|
||||
hint: const Text('Pilih Jawaban Benar'),
|
||||
isExpanded: true,
|
||||
items: List.generate(widget.optionTCList.length, (index) {
|
||||
final optionLabel = String.fromCharCode(65 + index); // 'A', 'B', 'C', etc.
|
||||
return DropdownMenuItem<String>(
|
||||
value: optionLabel,
|
||||
child: Text(optionLabel),
|
||||
);
|
||||
}),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
selectedCorrectAnswer = value;
|
||||
});
|
||||
},
|
||||
child: Obx(
|
||||
() => DropdownButtonHideUnderline(
|
||||
child: DropdownButton<int>(
|
||||
value: controller.selectedOptionIndex.value,
|
||||
isExpanded: true,
|
||||
items: List.generate(controller.optionTCList.length, (index) {
|
||||
final optionLabel = String.fromCharCode(65 + index); // 'A', 'B', 'C', etc.
|
||||
return DropdownMenuItem<int>(
|
||||
value: index,
|
||||
child: Text(optionLabel),
|
||||
);
|
||||
}),
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
controller.selectedOptionIndex.value = value;
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:quiz_app/component/global_text_field.dart';
|
||||
import 'package:quiz_app/component/label_text_field.dart';
|
||||
import 'package:quiz_app/feature/quiz_creation/controller/quiz_creation_controller.dart'; // Ganti path sesuai projekmu
|
||||
|
||||
class TrueFalseQuestionComponent extends StatefulWidget {
|
||||
class TrueFalseQuestionComponent extends GetView<QuizCreationController> {
|
||||
final TextEditingController questionTC;
|
||||
|
||||
const TrueFalseQuestionComponent({
|
||||
|
@ -10,13 +12,6 @@ class TrueFalseQuestionComponent extends StatefulWidget {
|
|||
required this.questionTC,
|
||||
});
|
||||
|
||||
@override
|
||||
State<TrueFalseQuestionComponent> createState() => _TrueFalseQuestionComponentState();
|
||||
}
|
||||
|
||||
class _TrueFalseQuestionComponentState extends State<TrueFalseQuestionComponent> {
|
||||
bool? selectedAnswer; // true or false
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
|
@ -24,13 +19,13 @@ class _TrueFalseQuestionComponentState extends State<TrueFalseQuestionComponent>
|
|||
// Pertanyaan
|
||||
LabelTextField(label: "Pertanyaan"),
|
||||
GlobalTextField(
|
||||
controller: widget.questionTC,
|
||||
controller: questionTC,
|
||||
limitTextLine: 3,
|
||||
hintText: "Tulis Pertanyaan",
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// Jawaban Dropdown
|
||||
// Jawaban Benar Dropdown
|
||||
LabelTextField(label: "Jawaban Benar"),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
|
@ -39,26 +34,28 @@ class _TrueFalseQuestionComponentState extends State<TrueFalseQuestionComponent>
|
|||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<bool>(
|
||||
value: selectedAnswer,
|
||||
hint: const Text('Pilih Jawaban Benar'),
|
||||
isExpanded: true,
|
||||
items: const [
|
||||
DropdownMenuItem(
|
||||
value: true,
|
||||
child: Text('True'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: false,
|
||||
child: Text('False'),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
selectedAnswer = value;
|
||||
});
|
||||
},
|
||||
child: Obx(
|
||||
() => DropdownButtonHideUnderline(
|
||||
child: DropdownButton<bool>(
|
||||
value: _getCurrentAnswer(),
|
||||
hint: const Text('Pilih Jawaban Benar'),
|
||||
isExpanded: true,
|
||||
items: const [
|
||||
DropdownMenuItem(
|
||||
value: true,
|
||||
child: Text('True'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: false,
|
||||
child: Text('False'),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
controller.updateTOFAnswer(value);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -67,4 +64,12 @@ class _TrueFalseQuestionComponentState extends State<TrueFalseQuestionComponent>
|
|||
],
|
||||
);
|
||||
}
|
||||
|
||||
bool? _getCurrentAnswer() {
|
||||
// Ambil answer dari controller dan parsing ke bool
|
||||
final currentAnswer = controller.quizData[controller.selectedQuizIndex.value].answer;
|
||||
if (currentAnswer == "true") return true;
|
||||
if (currentAnswer == "false") return false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:quiz_app/app/const/colors/app_colors.dart';
|
||||
import 'package:quiz_app/component/global_button.dart';
|
||||
import 'package:quiz_app/feature/quiz_creation/controller/quiz_creation_controller.dart';
|
||||
import 'package:quiz_app/feature/quiz_creation/view/component/custom_question_component.dart';
|
||||
import 'package:quiz_app/feature/quiz_creation/view/component/generate_component.dart';
|
||||
|
@ -40,6 +41,8 @@ class QuizCreationView extends GetView<QuizCreationController> {
|
|||
Obx(
|
||||
() => controller.isGenerate.value ? GenerateComponent() : CustomQuestionComponent(),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
GlobalButton(text: "simpan semua", onPressed: () {})
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
Loading…
Reference in New Issue