feat: done logic request on the add new quiz
This commit is contained in:
parent
7a90e7ea16
commit
effaa4cdd7
|
@ -5,4 +5,6 @@ class APIEndpoint {
|
||||||
static const String loginGoogle = "/login/google";
|
static const String loginGoogle = "/login/google";
|
||||||
|
|
||||||
static const String register = "/register";
|
static const String register = "/register";
|
||||||
|
|
||||||
|
static const String quiz = "/quiz";
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:quiz_app/core/utils/logger.dart';
|
||||||
import 'package:quiz_app/data/services/user_storage_service.dart';
|
import 'package:quiz_app/data/services/user_storage_service.dart';
|
||||||
|
|
||||||
class UserController extends GetxController {
|
class UserController extends GetxController {
|
||||||
|
@ -9,6 +10,7 @@ class UserController extends GetxController {
|
||||||
Rx<String> userName = "".obs;
|
Rx<String> userName = "".obs;
|
||||||
Rx<String?> userImage = Rx<String?>(null);
|
Rx<String?> userImage = Rx<String?>(null);
|
||||||
Rx<String> email = "".obs;
|
Rx<String> email = "".obs;
|
||||||
|
String userId = "";
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
|
@ -22,7 +24,9 @@ class UserController extends GetxController {
|
||||||
userName.value = data.name;
|
userName.value = data.name;
|
||||||
userImage.value = data.picUrl;
|
userImage.value = data.picUrl;
|
||||||
email.value = data.email;
|
email.value = data.email;
|
||||||
print("Loaded user: ${data.toJson()}");
|
userId = data.id ?? "";
|
||||||
|
logC.i("user data $userId");
|
||||||
|
logC.i("Loaded user: ${data.toJson()}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
class QuizCreateRequestModel {
|
||||||
|
final String title;
|
||||||
|
final String description;
|
||||||
|
final bool isPublic;
|
||||||
|
final String date;
|
||||||
|
final int totalQuiz;
|
||||||
|
final int limitDuration;
|
||||||
|
final String authorId;
|
||||||
|
final List<QuestionListing> questionListings;
|
||||||
|
|
||||||
|
QuizCreateRequestModel({
|
||||||
|
required this.title,
|
||||||
|
required this.description,
|
||||||
|
required this.isPublic,
|
||||||
|
required this.date,
|
||||||
|
required this.totalQuiz,
|
||||||
|
required this.limitDuration,
|
||||||
|
required this.authorId,
|
||||||
|
required this.questionListings,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'title': title,
|
||||||
|
'description': description,
|
||||||
|
'is_public': isPublic,
|
||||||
|
'date': date,
|
||||||
|
'total_quiz': totalQuiz,
|
||||||
|
'limit_duration': limitDuration,
|
||||||
|
'author_id': authorId,
|
||||||
|
'question_listings': questionListings.map((e) => e.toJson()).toList(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class QuestionListing {
|
||||||
|
final String question;
|
||||||
|
final String targetAnswer;
|
||||||
|
final int duration;
|
||||||
|
final String type;
|
||||||
|
final List<String>? options;
|
||||||
|
|
||||||
|
QuestionListing({
|
||||||
|
required this.question,
|
||||||
|
required this.targetAnswer,
|
||||||
|
required this.duration,
|
||||||
|
required this.type,
|
||||||
|
this.options,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final map = <String, dynamic>{
|
||||||
|
'question': question,
|
||||||
|
'target_answer': targetAnswer,
|
||||||
|
'duration': duration,
|
||||||
|
'type': type,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options != null && options!.isNotEmpty) {
|
||||||
|
map['options'] = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:quiz_app/core/endpoint/api_endpoint.dart';
|
import 'package:quiz_app/core/endpoint/api_endpoint.dart';
|
||||||
|
import 'package:quiz_app/core/utils/logger.dart';
|
||||||
|
|
||||||
class ApiClient extends GetxService {
|
class ApiClient extends GetxService {
|
||||||
late final Dio dio;
|
late final Dio dio;
|
||||||
|
@ -15,7 +16,37 @@ class ApiClient extends GetxService {
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
|
|
||||||
dio.interceptors.add(LogInterceptor(responseBody: true));
|
dio.interceptors.add(
|
||||||
|
InterceptorsWrapper(
|
||||||
|
onRequest: (options, handler) {
|
||||||
|
logC.i('''
|
||||||
|
➡️ [REQUEST]
|
||||||
|
[${options.method}] ${options.uri}
|
||||||
|
Headers: ${options.headers}
|
||||||
|
Body: ${options.data}
|
||||||
|
''');
|
||||||
|
return handler.next(options);
|
||||||
|
},
|
||||||
|
onResponse: (response, handler) {
|
||||||
|
logC.i('''
|
||||||
|
✅ [RESPONSE]
|
||||||
|
[${response.statusCode}] ${response.requestOptions.uri}
|
||||||
|
Data: ${response.data}
|
||||||
|
''');
|
||||||
|
return handler.next(response);
|
||||||
|
},
|
||||||
|
onError: (DioException e, handler) {
|
||||||
|
logC.e('''
|
||||||
|
❌ [ERROR]
|
||||||
|
[${e.response?.statusCode}] ${e.requestOptions.uri}
|
||||||
|
Message: ${e.message}
|
||||||
|
Error Data: ${e.response?.data}
|
||||||
|
''');
|
||||||
|
return handler.next(e);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:quiz_app/core/endpoint/api_endpoint.dart';
|
||||||
|
import 'package:quiz_app/data/models/quiz/question_create_request.dart';
|
||||||
|
import 'package:quiz_app/data/providers/dio_client.dart';
|
||||||
|
|
||||||
|
class QuizService extends GetxService {
|
||||||
|
late final Dio _dio;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
_dio = Get.find<ApiClient>().dio;
|
||||||
|
super.onInit();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> createQuiz(QuizCreateRequestModel request) async {
|
||||||
|
try {
|
||||||
|
final response = await _dio.post(
|
||||||
|
APIEndpoint.quiz,
|
||||||
|
data: request.toJson(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 201) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw Exception("Quiz creation failed");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception("Quiz creation error: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,8 +13,7 @@ class ProfileView extends GetView<ProfileController> {
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
child: Obx(() {
|
child: Obx(() {
|
||||||
return Column(
|
return ListView(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
_buildAvatar(),
|
_buildAvatar(),
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import 'package:get/get.dart';
|
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/feature/quiz_preview/controller/quiz_preview_controller.dart';
|
import 'package:quiz_app/feature/quiz_preview/controller/quiz_preview_controller.dart';
|
||||||
|
|
||||||
class QuizPreviewBinding extends Bindings {
|
class QuizPreviewBinding extends Bindings {
|
||||||
@override
|
@override
|
||||||
void dependencies() {
|
void dependencies() {
|
||||||
Get.lazyPut<QuizPreviewController>(() => QuizPreviewController());
|
Get.lazyPut<QuizService>(() => QuizService());
|
||||||
|
Get.lazyPut<QuizPreviewController>(() => QuizPreviewController(Get.find<QuizService>(), Get.find<UserController>()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,24 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:quiz_app/app/const/colors/app_colors.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/app/const/enums/question_type.dart';
|
||||||
|
import 'package:quiz_app/app/routes/app_pages.dart';
|
||||||
|
import 'package:quiz_app/core/utils/logger.dart';
|
||||||
|
import 'package:quiz_app/data/controllers/user_controller.dart';
|
||||||
|
import 'package:quiz_app/data/models/quiz/question_create_request.dart';
|
||||||
import 'package:quiz_app/data/models/quiz/quiestion_data_model.dart';
|
import 'package:quiz_app/data/models/quiz/quiestion_data_model.dart';
|
||||||
|
import 'package:quiz_app/data/services/quiz_service.dart';
|
||||||
|
|
||||||
class QuizPreviewController extends GetxController {
|
class QuizPreviewController extends GetxController {
|
||||||
final TextEditingController titleController = TextEditingController();
|
final TextEditingController titleController = TextEditingController();
|
||||||
final TextEditingController descriptionController = TextEditingController();
|
final TextEditingController descriptionController = TextEditingController();
|
||||||
|
|
||||||
|
final QuizService _quizService;
|
||||||
|
final UserController _userController;
|
||||||
|
|
||||||
|
QuizPreviewController(this._quizService, this._userController);
|
||||||
|
|
||||||
|
RxBool isPublic = false.obs;
|
||||||
|
|
||||||
late final List<QuestionData> data;
|
late final List<QuestionData> data;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -25,7 +37,7 @@ class QuizPreviewController extends GetxController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onSaveQuiz() {
|
Future<void> onSaveQuiz() async {
|
||||||
final title = titleController.text.trim();
|
final title = titleController.text.trim();
|
||||||
final description = descriptionController.text.trim();
|
final description = descriptionController.text.trim();
|
||||||
|
|
||||||
|
@ -34,7 +46,56 @@ class QuizPreviewController extends GetxController {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Get.snackbar('Sukses', 'Kuis berhasil disimpan!');
|
try {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final String formattedDate = "${now.day.toString().padLeft(2, '0')}-${now.month.toString().padLeft(2, '0')}-${now.year}";
|
||||||
|
|
||||||
|
final quizRequest = QuizCreateRequestModel(
|
||||||
|
title: title,
|
||||||
|
description: description,
|
||||||
|
isPublic: isPublic.value,
|
||||||
|
date: formattedDate,
|
||||||
|
totalQuiz: data.length,
|
||||||
|
limitDuration: data.length * 30,
|
||||||
|
authorId: _userController.userId,
|
||||||
|
questionListings: _mapQuestionsToListings(data),
|
||||||
|
);
|
||||||
|
final success = await _quizService.createQuiz(quizRequest);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
Get.snackbar('Sukses', 'Kuis berhasil disimpan!');
|
||||||
|
Get.offAllNamed(AppRoutes.mainPage);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logC.e(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<QuestionListing> _mapQuestionsToListings(List<QuestionData> questions) {
|
||||||
|
return questions.map((q) {
|
||||||
|
String typeString;
|
||||||
|
switch (q.type) {
|
||||||
|
case QuestionType.fillTheBlank:
|
||||||
|
typeString = 'fill_the_blank';
|
||||||
|
break;
|
||||||
|
case QuestionType.option:
|
||||||
|
typeString = 'option';
|
||||||
|
break;
|
||||||
|
case QuestionType.trueOrFalse:
|
||||||
|
typeString = 'true_false';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
typeString = 'fill_the_blank';
|
||||||
|
}
|
||||||
|
|
||||||
|
return QuestionListing(
|
||||||
|
question: q.question ?? '',
|
||||||
|
targetAnswer: q.answer ?? '',
|
||||||
|
duration: 30,
|
||||||
|
type: typeString,
|
||||||
|
options: q.options?.map((o) => o.text).toList(),
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildQuestionCard(QuestionData question) {
|
Widget buildQuestionCard(QuestionData question) {
|
||||||
|
|
|
@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:quiz_app/app/const/colors/app_colors.dart';
|
import 'package:quiz_app/app/const/colors/app_colors.dart';
|
||||||
import 'package:quiz_app/component/global_button.dart';
|
import 'package:quiz_app/component/global_button.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_preview/controller/quiz_preview_controller.dart';
|
import 'package:quiz_app/feature/quiz_preview/controller/quiz_preview_controller.dart';
|
||||||
|
|
||||||
class QuizPreviewPage extends GetView<QuizPreviewController> {
|
class QuizPreviewPage extends GetView<QuizPreviewController> {
|
||||||
|
@ -31,16 +33,13 @@ class QuizPreviewPage extends GetView<QuizPreviewController> {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildTextField(
|
LabelTextField(label: "Judul"),
|
||||||
label: 'Judul Kuis',
|
GlobalTextField(controller: controller.titleController),
|
||||||
controller: controller.titleController,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
_buildTextField(
|
LabelTextField(label: "Deskripsi Singkat"),
|
||||||
label: 'Deskripsi Kuis',
|
GlobalTextField(controller: controller.descriptionController),
|
||||||
controller: controller.descriptionController,
|
const SizedBox(height: 20),
|
||||||
maxLines: 3,
|
_buildPublicCheckbox(), // Ganti ke sini
|
||||||
),
|
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 30),
|
||||||
const Divider(thickness: 1.2, color: AppColors.borderLight),
|
const Divider(thickness: 1.2, color: AppColors.borderLight),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
@ -58,43 +57,6 @@ class QuizPreviewPage extends GetView<QuizPreviewController> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTextField({
|
|
||||||
required String label,
|
|
||||||
required TextEditingController controller,
|
|
||||||
int maxLines = 1,
|
|
||||||
}) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(label, style: const TextStyle(fontWeight: FontWeight.w600, color: AppColors.softGrayText)),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
TextField(
|
|
||||||
controller: controller,
|
|
||||||
maxLines: maxLines,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: 'Masukkan $label',
|
|
||||||
hintStyle: const TextStyle(color: AppColors.softGrayText),
|
|
||||||
filled: true,
|
|
||||||
fillColor: Colors.white,
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
borderSide: const BorderSide(color: AppColors.borderLight),
|
|
||||||
),
|
|
||||||
enabledBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
borderSide: const BorderSide(color: AppColors.borderLight),
|
|
||||||
),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
borderSide: const BorderSide(color: AppColors.primaryBlue, width: 2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildQuestionContent() {
|
Widget _buildQuestionContent() {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
@ -103,4 +65,42 @@ class QuizPreviewPage extends GetView<QuizPreviewController> {
|
||||||
}).toList(),
|
}).toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
side: const BorderSide(
|
||||||
|
color: AppColors.primaryBlue, // Pinggirannya juga biru
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue