develop #1

Merged
akhdanre merged 104 commits from develop into main 2025-07-10 12:38:53 +07:00
9 changed files with 318 additions and 73 deletions
Showing only changes of commit 80e6704bec - Show all commits

View File

@ -13,4 +13,6 @@ class APIEndpoint {
static const String historyQuiz = "/history";
static const String detailHistoryQuiz = "/history/detail";
static const String subject = "/subject";
}

View File

@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class CustomNotification {
static void _showSnackbar({
required String title,
required String message,
required IconData icon,
required Color backgroundColor,
Color textColor = Colors.white,
Color iconColor = Colors.white,
}) {
Get.snackbar(
title,
message,
icon: Icon(icon, color: iconColor),
backgroundColor: backgroundColor,
colorText: textColor,
borderRadius: 12,
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
snackPosition: SnackPosition.TOP,
duration: const Duration(seconds: 3),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
isDismissible: true,
forwardAnimationCurve: Curves.easeOutBack,
reverseAnimationCurve: Curves.easeInBack,
boxShadows: [
BoxShadow(
color: Colors.black26,
blurRadius: 10,
offset: Offset(0, 4),
),
],
);
}
static void success({required String title, required String message}) {
_showSnackbar(
title: title,
message: message,
icon: Icons.check_circle_outline,
backgroundColor: Colors.green.shade600,
);
}
static void error({required String title, required String message}) {
_showSnackbar(
title: title,
message: message,
icon: Icons.error_outline,
backgroundColor: Colors.red.shade600,
);
}
static void warning({required String title, required String message}) {
_showSnackbar(
title: title,
message: message,
icon: Icons.warning_amber_rounded,
backgroundColor: Colors.orange.shade700,
);
}
}

View File

@ -8,6 +8,7 @@ class QuizCreateRequestModel {
final int totalQuiz;
final int limitDuration;
final String authorId;
final String subjectId;
final List<QuestionListing> questionListings;
QuizCreateRequestModel({
@ -18,6 +19,7 @@ class QuizCreateRequestModel {
required this.totalQuiz,
required this.limitDuration,
required this.authorId,
required this.subjectId,
required this.questionListings,
});
@ -30,6 +32,7 @@ class QuizCreateRequestModel {
'total_quiz': totalQuiz,
'limit_duration': limitDuration,
'author_id': authorId,
"subject_id": subjectId,
'question_listings': questionListings.map((e) => e.toJson()).toList(),
};
}

View File

@ -0,0 +1,31 @@
class SubjectModel {
final String id;
final String name;
final String alias;
final String description;
SubjectModel({
required this.id,
required this.name,
required this.alias,
required this.description,
});
factory SubjectModel.fromJson(Map<String, dynamic> json) {
return SubjectModel(
id: json['id'],
name: json['name'],
alias: json['alias'],
description: json['description'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'alias': alias,
'description': description,
};
}
}

View File

@ -0,0 +1,39 @@
import 'package:dio/dio.dart';
import 'package:get/get.dart';
import 'package:quiz_app/core/endpoint/api_endpoint.dart';
import 'package:quiz_app/core/utils/logger.dart';
import 'package:quiz_app/data/models/base/base_model.dart';
import 'package:quiz_app/data/models/subject/subject_model.dart';
import 'package:quiz_app/data/providers/dio_client.dart';
class SubjectService extends GetxService {
late final Dio _dio;
@override
void onInit() {
_dio = Get.find<ApiClient>().dio;
super.onInit();
}
Future<BaseResponseModel<List<SubjectModel>>?> getSubject() async {
try {
final response = await _dio.get(
APIEndpoint.subject,
);
if (response.statusCode == 200) {
final parsedResponse = BaseResponseModel<List<SubjectModel>>.fromJson(
response.data,
(data) => (data as List).map((e) => SubjectModel.fromJson(e as Map<String, dynamic>)).toList(),
);
return parsedResponse;
} else {
return null;
}
} catch (e) {
logC.e("Quiz creation error: $e");
return null;
}
}
}

View File

@ -1,12 +1,18 @@
import 'package:get/get.dart';
import 'package:quiz_app/data/controllers/user_controller.dart';
import 'package:quiz_app/data/services/quiz_service.dart';
import 'package:quiz_app/data/services/subject_service.dart';
import 'package:quiz_app/feature/quiz_preview/controller/quiz_preview_controller.dart';
class QuizPreviewBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<QuizService>(() => QuizService());
Get.lazyPut<QuizPreviewController>(() => QuizPreviewController(Get.find<QuizService>(), Get.find<UserController>()));
Get.lazyPut<SubjectService>(() => SubjectService());
Get.lazyPut<QuizPreviewController>(() => QuizPreviewController(
Get.find<QuizService>(),
Get.find<UserController>(),
Get.find<SubjectService>(),
));
}
}

View File

@ -2,12 +2,16 @@ import 'package:flutter/material.dart';
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/core/utils/custom_notification.dart';
import 'package:quiz_app/core/utils/logger.dart';
import 'package:quiz_app/data/controllers/user_controller.dart';
import 'package:quiz_app/data/models/base/base_model.dart';
import 'package:quiz_app/data/models/quiz/question_create_request.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/subject/subject_model.dart';
import 'package:quiz_app/data/services/quiz_service.dart';
import 'package:quiz_app/data/services/subject_service.dart';
class QuizPreviewController extends GetxController {
final TextEditingController titleController = TextEditingController();
@ -15,17 +19,29 @@ class QuizPreviewController extends GetxController {
final QuizService _quizService;
final UserController _userController;
final SubjectService _subjectService;
QuizPreviewController(this._quizService, this._userController);
QuizPreviewController(
this._quizService,
this._userController,
this._subjectService,
);
RxBool isPublic = false.obs;
late final List<QuestionData> data;
RxList<SubjectModel> subjects = <SubjectModel>[].obs;
RxInt subjectIndex = 0.obs;
String subjectId = "";
@override
void onInit() {
super.onInit();
loadData();
loadSubjectData();
}
void loadData() {
@ -37,12 +53,31 @@ class QuizPreviewController extends GetxController {
}
}
void loadSubjectData() async {
BaseResponseModel<List<SubjectModel>>? respnse = await _subjectService.getSubject();
if (respnse != null) {
subjects.assignAll(respnse.data!);
subjectId = subjects[0].id;
}
}
Future<void> onSaveQuiz() async {
final title = titleController.text.trim();
final description = descriptionController.text.trim();
if (title.isEmpty || description.isEmpty) {
Get.snackbar('Error', 'Judul dan deskripsi tidak boleh kosong!');
CustomNotification.error(
title: 'Error',
message: 'Judul dan deskripsi tidak boleh kosong!',
);
return;
}
if (data.length < 10) {
CustomNotification.error(
title: 'Error',
message: 'Jumlah soal harus 10 atau lebih',
);
return;
}
@ -58,12 +93,17 @@ class QuizPreviewController extends GetxController {
totalQuiz: data.length,
limitDuration: data.length * 30,
authorId: _userController.userData!.id,
subjectId: subjectId,
questionListings: _mapQuestionsToListings(data),
);
final success = await _quizService.createQuiz(quizRequest);
if (success) {
Get.snackbar('Sukses', 'Kuis berhasil disimpan!');
CustomNotification.success(
title: 'Sukses',
message: 'Kuis berhasil disimpan!',
);
Get.offAllNamed(AppRoutes.mainPage, arguments: 2);
}
} catch (e) {
@ -110,6 +150,11 @@ class QuizPreviewController extends GetxController {
}).toList();
}
void onSubjectTap(String id, int index) {
subjectId = id;
subjectIndex.value = index;
}
@override
void onClose() {
titleController.dispose();

View File

@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:quiz_app/app/const/colors/app_colors.dart';
import 'package:quiz_app/data/models/subject/subject_model.dart';
class SubjectDropdownComponent extends StatelessWidget {
final List<SubjectModel> data;
final void Function(String id, int index) onItemTap;
final int selectedIndex;
const SubjectDropdownComponent({
super.key,
required this.data,
required this.onItemTap,
required this.selectedIndex,
});
@override
Widget build(BuildContext context) {
return DropdownButtonFormField<String>(
value: selectedIndex >= 0 && selectedIndex < data.length ? data[selectedIndex].id : null,
items: data.asMap().entries.map((entry) {
// final index = entry.key;
final subject = entry.value;
return DropdownMenuItem<String>(
value: subject.id,
child: Text('${subject.alias} - ${subject.name}'),
);
}).toList(),
onChanged: (value) {
if (value != null) {
final index = data.indexWhere((e) => e.id == value);
if (index != -1) {
onItemTap(value, index);
}
}
},
decoration: InputDecoration(
filled: true,
fillColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: AppColors.borderLight),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: AppColors.borderLight),
),
),
);
}
}

View File

@ -6,6 +6,7 @@ import 'package:quiz_app/component/global_text_field.dart';
import 'package:quiz_app/component/label_text_field.dart';
import 'package:quiz_app/component/widget/question_container_widget.dart';
import 'package:quiz_app/feature/quiz_preview/controller/quiz_preview_controller.dart';
import 'package:quiz_app/feature/quiz_preview/view/component/subject_dropdown_component.dart';
class QuizPreviewPage extends GetView<QuizPreviewController> {
const QuizPreviewPage({super.key});
@ -14,50 +15,65 @@ class QuizPreviewPage extends GetView<QuizPreviewController> {
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.background,
appBar: AppBar(
backgroundColor: AppColors.background,
elevation: 0,
title: const Text(
'Preview Quiz',
style: TextStyle(
color: AppColors.darkText,
fontWeight: FontWeight.bold,
),
),
centerTitle: true,
iconTheme: const IconThemeData(color: AppColors.darkText),
),
appBar: _buildAppBar(),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LabelTextField(label: "Judul"),
GlobalTextField(controller: controller.titleController),
const SizedBox(height: 20),
LabelTextField(label: "Deskripsi Singkat"),
GlobalTextField(controller: controller.descriptionController),
const SizedBox(height: 20),
_buildPublicCheckbox(), // Ganti ke sini
const SizedBox(height: 30),
const Divider(thickness: 1.2, color: AppColors.borderLight),
const SizedBox(height: 20),
_buildQuestionContent(),
const SizedBox(height: 30),
GlobalButton(
onPressed: controller.onSaveQuiz,
text: "Simpan Kuis",
),
],
),
),
child: _buildContent(),
),
),
);
}
PreferredSizeWidget _buildAppBar() {
return AppBar(
backgroundColor: AppColors.background,
elevation: 0,
centerTitle: true,
iconTheme: const IconThemeData(color: AppColors.darkText),
title: const Text(
'Preview Quiz',
style: TextStyle(
color: AppColors.darkText,
fontWeight: FontWeight.bold,
),
),
);
}
Widget _buildContent() {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const LabelTextField(label: "Judul"),
GlobalTextField(controller: controller.titleController),
const SizedBox(height: 20),
const LabelTextField(label: "Deskripsi Singkat"),
GlobalTextField(controller: controller.descriptionController),
const SizedBox(height: 20),
const LabelTextField(label: "Mata Pelajaran"),
Obx(() => SubjectDropdownComponent(
data: controller.subjects.toList(),
onItemTap: controller.onSubjectTap,
selectedIndex: controller.subjectIndex.value,
)),
const SizedBox(height: 20),
_buildPublicCheckbox(),
const SizedBox(height: 30),
const Divider(thickness: 1.2, color: AppColors.borderLight),
const SizedBox(height: 20),
_buildQuestionContent(),
const SizedBox(height: 30),
GlobalButton(
onPressed: controller.onSaveQuiz,
text: "Simpan Kuis",
),
],
),
);
}
Widget _buildQuestionContent() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -68,40 +84,28 @@ class QuizPreviewPage extends GetView<QuizPreviewController> {
}
Widget _buildPublicCheckbox() {
return Obx(
() => GestureDetector(
onTap: () {
controller.isPublic.toggle();
},
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Checkbox(
value: controller.isPublic.value,
activeColor: AppColors.primaryBlue, // Pakai warna biru utama
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
return Obx(() => GestureDetector(
onTap: controller.isPublic.toggle,
child: Row(
children: [
Checkbox(
value: controller.isPublic.value,
activeColor: AppColors.primaryBlue,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
side: const BorderSide(color: AppColors.primaryBlue, width: 2),
onChanged: (val) => controller.isPublic.value = val ?? false,
),
side: const BorderSide(
color: AppColors.primaryBlue, // Pinggirannya juga biru
width: 2,
const SizedBox(width: 8),
const Text(
"Buat Kuis Public",
style: TextStyle(
fontSize: 16,
color: AppColors.darkText,
fontWeight: FontWeight.w500,
),
),
onChanged: (value) {
controller.isPublic.value = value ?? false;
},
),
const SizedBox(width: 8),
const Text(
"Buat Kuis Public",
style: TextStyle(
fontSize: 16,
color: AppColors.darkText,
fontWeight: FontWeight.w500,
),
),
],
),
),
);
],
),
));
}
}