develop #1

Merged
akhdanre merged 104 commits from develop into main 2025-07-10 12:38:53 +07:00
7 changed files with 286 additions and 106 deletions
Showing only changes of commit e801962e4c - Show all commits

View File

@ -0,0 +1 @@
enum QuestionType { fillTheBlank, option, trueOrFalse }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: () {})
],
),
),