From cf9483834e10ef6bbc234b88cd6156af82dd0e0e Mon Sep 17 00:00:00 2001 From: akhdanre Date: Fri, 2 May 2025 00:16:45 +0700 Subject: [PATCH] feat: search endpoint --- lib/component/quiz_container_component.dart | 12 ++-- .../widget/recomendation_component.dart | 66 +++++++++++++++++++ lib/core/endpoint/api_endpoint.dart | 2 + lib/data/models/quiz/quiz_listing_model.dart | 39 +++++++++++ lib/data/services/history_service.dart | 1 + lib/data/services/quiz_service.dart | 42 ++++++++++++ lib/feature/home/binding/home_binding.dart | 4 +- .../home/controller/home_controller.dart | 20 ++++++ .../component/recomendation_component.dart | 36 ---------- lib/feature/home/view/home_page.dart | 9 ++- .../search/binding/search_binding.dart | 6 +- .../search/controller/search_controller.dart | 31 +++++++++ lib/feature/search/view/search_view.dart | 56 +++++++--------- 13 files changed, 245 insertions(+), 79 deletions(-) create mode 100644 lib/component/widget/recomendation_component.dart create mode 100644 lib/data/models/quiz/quiz_listing_model.dart delete mode 100644 lib/feature/home/view/component/recomendation_component.dart diff --git a/lib/component/quiz_container_component.dart b/lib/component/quiz_container_component.dart index b70cb57..2e09756 100644 --- a/lib/component/quiz_container_component.dart +++ b/lib/component/quiz_container_component.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; import 'package:quiz_app/app/const/colors/app_colors.dart'; +import 'package:quiz_app/data/models/quiz/quiz_listing_model.dart'; class QuizContainerComponent extends StatelessWidget { - const QuizContainerComponent({super.key}); + final QuizListingModel data; + const QuizContainerComponent({required this.data, super.key}); @override Widget build(BuildContext context) { @@ -40,8 +42,8 @@ class QuizContainerComponent extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - "Physics", + Text( + data.title, style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, @@ -49,8 +51,8 @@ class QuizContainerComponent extends StatelessWidget { ), ), const SizedBox(height: 4), - const Text( - "created by Akhdan Rabbani", + Text( + "created by ${data.authorName}", style: TextStyle( fontSize: 12, color: Color(0xFF6B778C), diff --git a/lib/component/widget/recomendation_component.dart b/lib/component/widget/recomendation_component.dart new file mode 100644 index 0000000..3cfb4a1 --- /dev/null +++ b/lib/component/widget/recomendation_component.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:quiz_app/component/quiz_container_component.dart'; +import 'package:quiz_app/data/models/quiz/quiz_listing_model.dart'; + +class RecomendationComponent extends StatelessWidget { + final String title; + final List datas; + const RecomendationComponent({ + required this.title, + required this.datas, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionTitle(title), + const SizedBox(height: 10), + datas.isNotEmpty + // ? Text("yeay ${datas.length}") + ? ListView.builder( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: datas.length, + itemBuilder: (context, index) => QuizContainerComponent(data: datas[index]), + ) + : SizedBox.shrink() + ], + ); + } + + // Widget _label() { + // return const Padding( + // padding: EdgeInsets.symmetric(horizontal: 16), + // child: Text( + // "Quiz Recommendation", + // style: TextStyle( + // fontSize: 18, + // fontWeight: FontWeight.bold, + // color: Color(0xFF172B4D), // dark text + // ), + // ), + // ); + // } + + Widget _buildSectionTitle(String title) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + Text( + "Lihat semua", + style: TextStyle(fontSize: 14, color: Colors.blue.shade700), + ), + ], + ), + ); + } +} diff --git a/lib/core/endpoint/api_endpoint.dart b/lib/core/endpoint/api_endpoint.dart index 8416918..e1c5aec 100644 --- a/lib/core/endpoint/api_endpoint.dart +++ b/lib/core/endpoint/api_endpoint.dart @@ -8,6 +8,8 @@ class APIEndpoint { static const String quiz = "/quiz"; static const String userQuiz = "/quiz/user"; + static const String quizRecomendation = "/quiz/recomendation"; + static const String quizSearch = "/quiz/search"; static const String historyQuiz = "/history"; } diff --git a/lib/data/models/quiz/quiz_listing_model.dart b/lib/data/models/quiz/quiz_listing_model.dart new file mode 100644 index 0000000..7f9bfc6 --- /dev/null +++ b/lib/data/models/quiz/quiz_listing_model.dart @@ -0,0 +1,39 @@ +class QuizListingModel { + final String quizId; + final String authorId; + final String authorName; + final String title; + final String description; + final String date; + + QuizListingModel({ + required this.quizId, + required this.authorId, + required this.authorName, + required this.title, + required this.description, + required this.date, + }); + + factory QuizListingModel.fromJson(Map json) { + return QuizListingModel( + quizId: json['quiz_id'] as String, + authorId: json['author_id'] as String, + authorName: json['author_name'] as String, + title: json['title'] as String, + description: json['description'] as String, + date: json['date'] as String, + ); + } + + Map toJson() { + return { + 'quiz_id': quizId, + 'author_id': authorId, + 'author_name': authorName, + 'title': title, + 'description': description, + 'date': date, + }; + } +} diff --git a/lib/data/services/history_service.dart b/lib/data/services/history_service.dart index 1bfe06c..fa200f7 100644 --- a/lib/data/services/history_service.dart +++ b/lib/data/services/history_service.dart @@ -26,6 +26,7 @@ class HistoryService extends GetxService { return parsedResponse.data; } catch (e, stacktrace) { logC.e(e, stackTrace: stacktrace); + return null; } } } diff --git a/lib/data/services/quiz_service.dart b/lib/data/services/quiz_service.dart index f1397ba..8a46eaa 100644 --- a/lib/data/services/quiz_service.dart +++ b/lib/data/services/quiz_service.dart @@ -5,6 +5,8 @@ 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/models/quiz/question_listings_model.dart'; +import 'package:quiz_app/data/models/quiz/quiz_listing_model.dart'; import 'package:quiz_app/data/providers/dio_client.dart'; class QuizService extends GetxService { @@ -49,4 +51,44 @@ class QuizService extends GetxService { return []; } } + + Future>?> recomendationQuiz() async { + try { + final response = await _dio.get(APIEndpoint.quizRecomendation); + + if (response.statusCode == 200) { + final parsedResponse = BaseResponseModel>.fromJson( + response.data, + (data) => (data as List).map((e) => QuizListingModel.fromJson(e as Map)).toList(), + ); + return parsedResponse; + } else { + logC.e("Failed to fetch recommendation quizzes. Status: ${response.statusCode}"); + return null; + } + } catch (e) { + logC.e("Error fetching recommendation quizzes: $e"); + return null; + } + } + + Future>?> searchQuiz(String keyword) async { + try { + final response = await _dio.get("${APIEndpoint.quizSearch}?keyword=$keyword&page=1&limit=10"); + + if (response.statusCode == 200) { + final parsedResponse = BaseResponseModel>.fromJson( + response.data, + (data) => (data as List).map((e) => QuizListingModel.fromJson(e as Map)).toList(), + ); + return parsedResponse; + } else { + logC.e("Failed to fetch search quizzes. Status: ${response.statusCode}"); + return null; + } + } catch (e) { + logC.e("Error fetching search quizzes: $e"); + return null; + } + } } diff --git a/lib/feature/home/binding/home_binding.dart b/lib/feature/home/binding/home_binding.dart index 9adecc5..53a5d45 100644 --- a/lib/feature/home/binding/home_binding.dart +++ b/lib/feature/home/binding/home_binding.dart @@ -1,9 +1,11 @@ import 'package:get/get.dart'; +import 'package:quiz_app/data/services/quiz_service.dart'; import 'package:quiz_app/feature/home/controller/home_controller.dart'; class HomeBinding extends Bindings { @override void dependencies() { - Get.lazyPut(() => HomeController()); + Get.lazyPut(() => QuizService()); + Get.lazyPut(() => HomeController(Get.find())); } } diff --git a/lib/feature/home/controller/home_controller.dart b/lib/feature/home/controller/home_controller.dart index 862f335..0b74bac 100644 --- a/lib/feature/home/controller/home_controller.dart +++ b/lib/feature/home/controller/home_controller.dart @@ -1,12 +1,32 @@ import 'package:get/get.dart'; import 'package:quiz_app/app/routes/app_pages.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/quiz_listing_model.dart'; +import 'package:quiz_app/data/services/quiz_service.dart'; class HomeController extends GetxController { final UserController _userController = Get.find(); + QuizService _quizService; + HomeController(this._quizService); Rx get userName => _userController.userName; Rx get userImage => _userController.userImage; + RxList data = [].obs; + void goToQuizCreation() => Get.toNamed(AppRoutes.quizCreatePage); + + @override + void onInit() { + _getRecomendationQuiz(); + super.onInit(); + } + + void _getRecomendationQuiz() async { + BaseResponseModel? response = await _quizService.recomendationQuiz(); + if (response != null) { + data.assignAll(response.data as List); + } + } } diff --git a/lib/feature/home/view/component/recomendation_component.dart b/lib/feature/home/view/component/recomendation_component.dart deleted file mode 100644 index a0e4db4..0000000 --- a/lib/feature/home/view/component/recomendation_component.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:quiz_app/component/quiz_container_component.dart'; - -class RecomendationComponent extends StatelessWidget { - const RecomendationComponent({super.key}); - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _label(), - const SizedBox(height: 10), - QuizContainerComponent(), - const SizedBox(height: 10), - QuizContainerComponent(), - const SizedBox(height: 10), - QuizContainerComponent() - ], - ); - } - - Widget _label() { - return const Padding( - padding: EdgeInsets.symmetric(horizontal: 16), - child: Text( - "Quiz Recommendation", - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Color(0xFF172B4D), // dark text - ), - ), - ); - } -} diff --git a/lib/feature/home/view/home_page.dart b/lib/feature/home/view/home_page.dart index a696a49..5874e55 100644 --- a/lib/feature/home/view/home_page.dart +++ b/lib/feature/home/view/home_page.dart @@ -3,7 +3,7 @@ import 'package:get/get.dart'; import 'package:quiz_app/app/const/colors/app_colors.dart'; import 'package:quiz_app/feature/home/controller/home_controller.dart'; import 'package:quiz_app/feature/home/view/component/button_option.dart'; -import 'package:quiz_app/feature/home/view/component/recomendation_component.dart'; +import 'package:quiz_app/component/widget/recomendation_component.dart'; import 'package:quiz_app/feature/home/view/component/search_component.dart'; import 'package:quiz_app/feature/home/view/component/user_gretings.dart'; @@ -43,7 +43,12 @@ class HomeView extends GetView { children: [ SearchComponent(), const SizedBox(height: 20), - RecomendationComponent(), + Obx( + () => RecomendationComponent( + title: "Quiz Rekomendasi", + datas: controller.data.toList(), + ), + ), ], ), ), diff --git a/lib/feature/search/binding/search_binding.dart b/lib/feature/search/binding/search_binding.dart index 8ea4ccf..2d2c0f4 100644 --- a/lib/feature/search/binding/search_binding.dart +++ b/lib/feature/search/binding/search_binding.dart @@ -1,9 +1,13 @@ import 'package:get/get.dart'; +import 'package:quiz_app/data/services/quiz_service.dart'; import 'package:quiz_app/feature/search/controller/search_controller.dart'; class SearchBinding extends Bindings { @override void dependencies() { - Get.lazyPut(() => SearchQuizController()); + if (!Get.isRegistered()) { + Get.lazyPut(() => QuizService()); + } + Get.lazyPut(() => SearchQuizController(Get.find())); } } diff --git a/lib/feature/search/controller/search_controller.dart b/lib/feature/search/controller/search_controller.dart index f8c5808..da1e848 100644 --- a/lib/feature/search/controller/search_controller.dart +++ b/lib/feature/search/controller/search_controller.dart @@ -1,16 +1,47 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:quiz_app/data/models/base/base_model.dart'; +import 'package:quiz_app/data/models/quiz/quiz_listing_model.dart'; +import 'package:quiz_app/data/services/quiz_service.dart'; class SearchQuizController extends GetxController { + final QuizService _quizService; + + SearchQuizController(this._quizService); + final searchController = TextEditingController(); final searchText = ''.obs; + RxList recommendationQData = [].obs; + RxList searchQData = [].obs; + @override void onInit() { + getRecomendation(); super.onInit(); searchController.addListener(() { searchText.value = searchController.text; }); + debounce( + searchText, + (value) => getSearchData(value), + time: Duration(seconds: 2), + ); + } + + void getRecomendation() async { + BaseResponseModel? response = await _quizService.recomendationQuiz(); + if (response != null) { + recommendationQData.assignAll(response.data as List); + } + } + + void getSearchData(String keyword) async { + searchQData.clear(); + BaseResponseModel? response = await _quizService.searchQuiz(keyword); + if (response != null) { + searchQData.assignAll(response.data); + } } @override diff --git a/lib/feature/search/view/search_view.dart b/lib/feature/search/view/search_view.dart index b985385..248d695 100644 --- a/lib/feature/search/view/search_view.dart +++ b/lib/feature/search/view/search_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:quiz_app/component/quiz_container_component.dart'; +import 'package:quiz_app/component/widget/recomendation_component.dart'; import 'package:quiz_app/feature/search/controller/search_controller.dart'; class SearchView extends GetView { @@ -15,7 +16,6 @@ class SearchView extends GetView { padding: const EdgeInsets.all(16), child: Obx(() { final isSearching = controller.searchText.isNotEmpty; - return ListView( children: [ _buildSearchBar(), @@ -23,20 +23,24 @@ class SearchView extends GetView { if (isSearching) ...[ _buildCategoryFilter(), const SizedBox(height: 20), - const Text( - "Result", - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), - ), - const SizedBox(height: 10), - _buildQuizList(count: 5), + + ...controller.searchQData.map( + (e) => QuizContainerComponent(data: e), + ) ] else ...[ - _buildSectionTitle("Rekomendasi Quiz"), - const SizedBox(height: 10), - _buildQuizList(), + Obx( + () => RecomendationComponent( + title: "Quiz Rekomendasi", + datas: controller.recommendationQData.toList(), + ), + ), const SizedBox(height: 30), - _buildSectionTitle("Quiz Populer"), - const SizedBox(height: 10), - _buildQuizList(), + Obx( + () => RecomendationComponent( + title: "Quiz Populer", + datas: controller.recommendationQData.toList(), + ), + ), ], ], ); @@ -87,25 +91,9 @@ class SearchView extends GetView { ); } - Widget _buildSectionTitle(String title) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - title, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - Text( - "Lihat semua", - style: TextStyle(fontSize: 14, color: Colors.blue.shade700), - ), - ], - ); - } - - Widget _buildQuizList({int count = 3}) { - return Column( - children: List.generate(count, (_) => const QuizContainerComponent()), - ); - } + // Widget _buildQuizList({int count = 3}) { + // return Column( + // children: List.generate(count, (_) => const QuizContainerComponent()), + // ); + // } }