diff --git a/lib/core/endpoint/api_endpoint.dart b/lib/core/endpoint/api_endpoint.dart index 7f975a4..72da61f 100644 --- a/lib/core/endpoint/api_endpoint.dart +++ b/lib/core/endpoint/api_endpoint.dart @@ -5,4 +5,6 @@ class APIEndpoint { static const String loginGoogle = "/login/google"; static const String register = "/register"; + + static const String quiz = "/quiz"; } diff --git a/lib/data/controllers/user_controller.dart b/lib/data/controllers/user_controller.dart index 28a5ef4..5cda711 100644 --- a/lib/data/controllers/user_controller.dart +++ b/lib/data/controllers/user_controller.dart @@ -1,4 +1,5 @@ import 'package:get/get.dart'; +import 'package:quiz_app/core/utils/logger.dart'; import 'package:quiz_app/data/services/user_storage_service.dart'; class UserController extends GetxController { @@ -9,6 +10,7 @@ class UserController extends GetxController { Rx userName = "".obs; Rx userImage = Rx(null); Rx email = "".obs; + String userId = ""; @override void onInit() { @@ -22,7 +24,9 @@ class UserController extends GetxController { userName.value = data.name; userImage.value = data.picUrl; email.value = data.email; - print("Loaded user: ${data.toJson()}"); + userId = data.id ?? ""; + logC.i("user data $userId"); + logC.i("Loaded user: ${data.toJson()}"); } } } diff --git a/lib/data/models/quiz/question_create_request.dart b/lib/data/models/quiz/question_create_request.dart new file mode 100644 index 0000000..b535fe2 --- /dev/null +++ b/lib/data/models/quiz/question_create_request.dart @@ -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 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 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? options; + + QuestionListing({ + required this.question, + required this.targetAnswer, + required this.duration, + required this.type, + this.options, + }); + + Map toJson() { + final map = { + 'question': question, + 'target_answer': targetAnswer, + 'duration': duration, + 'type': type, + }; + + if (options != null && options!.isNotEmpty) { + map['options'] = options; + } + + return map; + } +} diff --git a/lib/data/providers/dio_client.dart b/lib/data/providers/dio_client.dart index c92b958..11795fc 100644 --- a/lib/data/providers/dio_client.dart +++ b/lib/data/providers/dio_client.dart @@ -1,6 +1,7 @@ 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'; class ApiClient extends GetxService { 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; } } diff --git a/lib/data/services/quiz_service.dart b/lib/data/services/quiz_service.dart new file mode 100644 index 0000000..7067a9e --- /dev/null +++ b/lib/data/services/quiz_service.dart @@ -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().dio; + super.onInit(); + } + + Future 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"); + } + } +} diff --git a/lib/feature/profile/view/profile_view.dart b/lib/feature/profile/view/profile_view.dart index cfc0cd0..a911402 100644 --- a/lib/feature/profile/view/profile_view.dart +++ b/lib/feature/profile/view/profile_view.dart @@ -13,8 +13,7 @@ class ProfileView extends GetView { child: Padding( padding: const EdgeInsets.all(20), child: Obx(() { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, + return ListView( children: [ const SizedBox(height: 20), _buildAvatar(), diff --git a/lib/feature/quiz_preview/binding/quiz_preview_binding.dart b/lib/feature/quiz_preview/binding/quiz_preview_binding.dart index 2403b46..7d3c4d7 100644 --- a/lib/feature/quiz_preview/binding/quiz_preview_binding.dart +++ b/lib/feature/quiz_preview/binding/quiz_preview_binding.dart @@ -1,9 +1,12 @@ 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'; class QuizPreviewBinding extends Bindings { @override void dependencies() { - Get.lazyPut(() => QuizPreviewController()); + Get.lazyPut(() => QuizService()); + Get.lazyPut(() => QuizPreviewController(Get.find(), Get.find())); } } diff --git a/lib/feature/quiz_preview/controller/quiz_preview_controller.dart b/lib/feature/quiz_preview/controller/quiz_preview_controller.dart index edcf152..512cc01 100644 --- a/lib/feature/quiz_preview/controller/quiz_preview_controller.dart +++ b/lib/feature/quiz_preview/controller/quiz_preview_controller.dart @@ -2,12 +2,24 @@ 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/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/services/quiz_service.dart'; class QuizPreviewController extends GetxController { final TextEditingController titleController = TextEditingController(); final TextEditingController descriptionController = TextEditingController(); + final QuizService _quizService; + final UserController _userController; + + QuizPreviewController(this._quizService, this._userController); + + RxBool isPublic = false.obs; + late final List data; @override @@ -25,7 +37,7 @@ class QuizPreviewController extends GetxController { } } - void onSaveQuiz() { + Future onSaveQuiz() async { final title = titleController.text.trim(); final description = descriptionController.text.trim(); @@ -34,7 +46,56 @@ class QuizPreviewController extends GetxController { 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 _mapQuestionsToListings(List 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) { diff --git a/lib/feature/quiz_preview/view/quiz_preview.dart b/lib/feature/quiz_preview/view/quiz_preview.dart index 21fa974..48b721c 100644 --- a/lib/feature/quiz_preview/view/quiz_preview.dart +++ b/lib/feature/quiz_preview/view/quiz_preview.dart @@ -2,6 +2,8 @@ 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/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'; class QuizPreviewPage extends GetView { @@ -31,16 +33,13 @@ class QuizPreviewPage extends GetView { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildTextField( - label: 'Judul Kuis', - controller: controller.titleController, - ), + LabelTextField(label: "Judul"), + GlobalTextField(controller: controller.titleController), const SizedBox(height: 20), - _buildTextField( - label: 'Deskripsi Kuis', - controller: controller.descriptionController, - maxLines: 3, - ), + 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), @@ -58,43 +57,6 @@ class QuizPreviewPage extends GetView { ); } - 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() { return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -103,4 +65,42 @@ class QuizPreviewPage extends GetView { }).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, + ), + ), + ], + ), + ), + ); + } }