diff --git a/lib/core/endpoint/api_endpoint.dart b/lib/core/endpoint/api_endpoint.dart index 72da61f..92f5238 100644 --- a/lib/core/endpoint/api_endpoint.dart +++ b/lib/core/endpoint/api_endpoint.dart @@ -7,4 +7,5 @@ class APIEndpoint { static const String register = "/register"; static const String quiz = "/quiz"; + static const String userQuiz = "/quiz/user"; } diff --git a/lib/data/models/base/base_model.dart b/lib/data/models/base/base_model.dart index 9e1e6ea..d5aac4e 100644 --- a/lib/data/models/base/base_model.dart +++ b/lib/data/models/base/base_model.dart @@ -1,7 +1,7 @@ class BaseResponseModel { final String message; final T? data; - final dynamic meta; + final MetaModel? meta; BaseResponseModel({ required this.message, @@ -11,12 +11,44 @@ class BaseResponseModel { factory BaseResponseModel.fromJson( Map json, - T Function(Map) fromJsonT, + T Function(dynamic) fromJsonT, ) { return BaseResponseModel( message: json['message'], data: json['data'] != null ? fromJsonT(json['data']) : null, - meta: json['meta'], + meta: json['meta'] != null ? MetaModel.fromJson(json['meta']) : null, ); } } + +class MetaModel { + final int totalPage; + final int currentPage; + final int totalData; + final int totalAllData; + + MetaModel({ + required this.totalPage, + required this.currentPage, + required this.totalData, + required this.totalAllData, + }); + + factory MetaModel.fromJson(Map json) { + return MetaModel( + totalPage: json['total_page'], + currentPage: json['current_page'], + totalData: json['total_data'], + totalAllData: json['total_all_data'], + ); + } + + Map toJson() { + return { + 'total_page': totalPage, + 'current_page': currentPage, + 'total_data': totalData, + 'total_all_data': totalAllData, + }; + } +} diff --git a/lib/data/models/quiz/library_quiz_model.dart b/lib/data/models/quiz/library_quiz_model.dart new file mode 100644 index 0000000..8dc601e --- /dev/null +++ b/lib/data/models/quiz/library_quiz_model.dart @@ -0,0 +1,83 @@ +class QuizData { + final String authorId; + final String title; + final String? description; + final bool isPublic; + final String? date; + final int totalQuiz; + final int limitDuration; + final List questionListings; + + QuizData({ + required this.authorId, + required this.title, + this.description, + required this.isPublic, + this.date, + required this.totalQuiz, + required this.limitDuration, + required this.questionListings, + }); + + factory QuizData.fromJson(Map json) { + return QuizData( + authorId: json['author_id'], + title: json['title'], + description: json['description'], + isPublic: json['is_public'], + date: json['date'], + totalQuiz: json['total_quiz'], + limitDuration: json['limit_duration'], + questionListings: (json['question_listings'] as List).map((e) => QuestionListing.fromJson(e)).toList(), + ); + } + + Map toJson() { + return { + 'author_id': authorId, + 'title': title, + 'description': description, + 'is_public': isPublic, + 'date': date, + 'total_quiz': totalQuiz, + 'limit_duration': limitDuration, + '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, + }); + + factory QuestionListing.fromJson(Map json) { + return QuestionListing( + question: json['question'], + targetAnswer: json['target_answer'], + duration: json['duration'], + type: json['type'], + options: json['options'] != null ? List.from(json['options']) : null, + ); + } + + Map toJson() { + return { + 'question': question, + 'target_answer': targetAnswer, + 'duration': duration, + 'type': type, + 'options': options, + }; + } +} diff --git a/lib/data/services/quiz_service.dart b/lib/data/services/quiz_service.dart index 7067a9e..f1397ba 100644 --- a/lib/data/services/quiz_service.dart +++ b/lib/data/services/quiz_service.dart @@ -1,6 +1,9 @@ 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/quiz/library_quiz_model.dart'; import 'package:quiz_app/data/models/quiz/question_create_request.dart'; import 'package:quiz_app/data/providers/dio_client.dart'; @@ -26,7 +29,24 @@ class QuizService extends GetxService { throw Exception("Quiz creation failed"); } } catch (e) { + logC.e("Quiz creation error: $e"); throw Exception("Quiz creation error: $e"); } } + + Future> userQuiz(String userId, int page) async { + try { + final response = await _dio.get("${APIEndpoint.userQuiz}/$userId?page=$page"); + + final parsedResponse = BaseResponseModel>.fromJson( + response.data, + (data) => (data as List).map((e) => QuizData.fromJson(e as Map)).toList(), + ); + + return parsedResponse.data ?? []; + } catch (e) { + logC.e("Error fetching user quizzes: $e"); + return []; + } + } } diff --git a/lib/feature/library/binding/library_binding.dart b/lib/feature/library/binding/library_binding.dart index 2ff7624..854c38f 100644 --- a/lib/feature/library/binding/library_binding.dart +++ b/lib/feature/library/binding/library_binding.dart @@ -1,9 +1,13 @@ 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/library/controller/library_controller.dart'; class LibraryBinding extends Bindings { @override void dependencies() { - Get.lazyPut(() => LibraryController()); + Get.lazyPut(() => QuizService()); + + Get.lazyPut(() => LibraryController(Get.find(), Get.find())); } } diff --git a/lib/feature/library/controller/library_controller.dart b/lib/feature/library/controller/library_controller.dart index eb45563..bf24873 100644 --- a/lib/feature/library/controller/library_controller.dart +++ b/lib/feature/library/controller/library_controller.dart @@ -1,27 +1,39 @@ import 'package:get/get.dart'; +import 'package:quiz_app/data/controllers/user_controller.dart'; +import 'package:quiz_app/data/models/quiz/library_quiz_model.dart'; +import 'package:quiz_app/data/services/quiz_service.dart'; class LibraryController extends GetxController { - RxList> quizList = >[].obs; + RxList quizs = [].obs; + RxBool isLoading = true.obs; + RxString emptyMessage = "".obs; + + final QuizService _quizService; + final UserController _userController; + LibraryController(this._quizService, this._userController); + + int currentPage = 1; @override void onInit() { + loadUserQuiz(); super.onInit(); - loadDummyQuiz(); } - void loadDummyQuiz() { - quizList.assignAll([ - { - "author_id": "user_12345", - "title": "Sejarah Indonesia - Kerajaan Hindu Budha", - "description": "Kuis ini membahas kerajaan-kerajaan Hindu Budha di Indonesia seperti Kutai, Sriwijaya, dan Majapahit.", - "is_public": true, - "date": "2025-04-25 10:00:00", - "total_quiz": 3, - "limit_duration": 900, - }, - // Tambahkan data dummy lain kalau mau - ]); + void loadUserQuiz() async { + try { + isLoading.value = true; + List data = await _quizService.userQuiz(_userController.userData!.id, currentPage); + if (data.isEmpty) { + emptyMessage.value = "Kamu belum membuat soal."; + } else { + quizs.addAll(data); + } + } catch (e) { + emptyMessage.value = "Terjadi kesalahan saat memuat data."; + } finally { + isLoading.value = false; + } } String formatDuration(int seconds) { @@ -32,7 +44,7 @@ class LibraryController extends GetxController { String formatDate(String dateString) { try { // DateTime date = DateTime.parse(dateString); - return "19-04-2025"; + return "19-04-2025"; // Ini kamu hardcode, pastikan nanti parse bener } catch (e) { return '-'; } diff --git a/lib/feature/library/view/library_view.dart b/lib/feature/library/view/library_view.dart index 98b60ff..11c8da3 100644 --- a/lib/feature/library/view/library_view.dart +++ b/lib/feature/library/view/library_view.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:quiz_app/data/models/quiz/library_quiz_model.dart'; import 'package:quiz_app/feature/library/controller/library_controller.dart'; class LibraryView extends GetView { @@ -33,15 +34,40 @@ class LibraryView extends GetView { ), const SizedBox(height: 20), Expanded( - child: Obx( - () => ListView.builder( - itemCount: controller.quizList.length, + child: Obx(() { + if (controller.isLoading.value) { + return const Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator(), + SizedBox(height: 12), + Text( + "Memuat data...", + style: TextStyle(color: Colors.grey, fontSize: 14), + ), + ], + ), + ); + } + + if (controller.quizs.isEmpty) { + return const Center( + child: Text( + "Belum ada soal tersedia.", + style: TextStyle(color: Colors.grey, fontSize: 14), + ), + ); + } + + return ListView.builder( + itemCount: controller.quizs.length, itemBuilder: (context, index) { - final quiz = controller.quizList[index]; + final quiz = controller.quizs[index]; return _buildQuizCard(quiz); }, - ), - ), + ); + }), ), ], ), @@ -50,7 +76,7 @@ class LibraryView extends GetView { ); } - Widget _buildQuizCard(Map quiz) { + Widget _buildQuizCard(QuizData quiz) { return Container( margin: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.all(16), @@ -82,7 +108,7 @@ class LibraryView extends GetView { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - quiz['title'] ?? '-', + quiz.title, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, @@ -93,7 +119,7 @@ class LibraryView extends GetView { ), const SizedBox(height: 4), Text( - quiz['description'] ?? '-', + quiz.description ?? "", style: const TextStyle( color: Colors.grey, fontSize: 12, @@ -107,21 +133,21 @@ class LibraryView extends GetView { const Icon(Icons.calendar_today_rounded, size: 14, color: Colors.grey), const SizedBox(width: 4), Text( - controller.formatDate(quiz['date']), + controller.formatDate(quiz.date ?? ""), style: const TextStyle(fontSize: 12, color: Colors.grey), ), const SizedBox(width: 12), const Icon(Icons.list, size: 14, color: Colors.grey), const SizedBox(width: 4), Text( - '${quiz['total_quiz']} Quizzes', + '${quiz.totalQuiz} Quizzes', style: const TextStyle(fontSize: 12, color: Colors.grey), ), const SizedBox(width: 12), const Icon(Icons.access_time, size: 14, color: Colors.grey), const SizedBox(width: 4), Text( - controller.formatDuration(quiz['limit_duration'] ?? 0), + controller.formatDuration(quiz.limitDuration), style: const TextStyle(fontSize: 12, color: Colors.grey), ), ], diff --git a/lib/feature/quiz_preview/controller/quiz_preview_controller.dart b/lib/feature/quiz_preview/controller/quiz_preview_controller.dart index e37d6d5..37d16b9 100644 --- a/lib/feature/quiz_preview/controller/quiz_preview_controller.dart +++ b/lib/feature/quiz_preview/controller/quiz_preview_controller.dart @@ -74,23 +74,28 @@ class QuizPreviewController extends GetxController { List _mapQuestionsToListings(List questions) { return questions.map((q) { String typeString; + String answer = ""; switch (q.type) { case QuestionType.fillTheBlank: typeString = 'fill_the_blank'; + answer = q.answer ?? ""; break; case QuestionType.option: typeString = 'option'; + answer = q.correctAnswerIndex.toString(); break; case QuestionType.trueOrFalse: typeString = 'true_false'; + answer = q.answer ?? ""; break; default: typeString = 'fill_the_blank'; + answer = q.answer ?? ""; } return QuestionListing( question: q.question ?? '', - targetAnswer: q.answer ?? '', + targetAnswer: answer, duration: 30, type: typeString, options: q.options?.map((o) => o.text).toList(),