diff --git a/lib/core/container/export_vmod.dart b/lib/core/container/export_vmod.dart index a1c0a9e..c0c4a15 100644 --- a/lib/core/container/export_vmod.dart +++ b/lib/core/container/export_vmod.dart @@ -14,3 +14,6 @@ export 'package:rijig_mobile/globaldata/trash/trash_viewmodel.dart'; export 'package:rijig_mobile/features/home/presentation/viewmodel/about_vmod.dart'; export 'package:rijig_mobile/features/home/repositories/about_repository.dart'; export 'package:rijig_mobile/features/home/service/about_service.dart'; +export 'package:rijig_mobile/globaldata/article/article_repository.dart'; +export 'package:rijig_mobile/globaldata/article/article_service.dart'; +export 'package:rijig_mobile/globaldata/article/article_vmod.dart'; diff --git a/lib/core/container/injection_container.dart b/lib/core/container/injection_container.dart index dde20c8..68bf8c0 100644 --- a/lib/core/container/injection_container.dart +++ b/lib/core/container/injection_container.dart @@ -12,4 +12,5 @@ void init() { ); sl.registerFactory(() => AboutViewModel(AboutService(AboutRepository()))); sl.registerFactory(() => AboutDetailViewModel(AboutService(AboutRepository()))); + sl.registerFactory(() => ArticleViewModel(ArticleService(ArticleRepository()))); } diff --git a/lib/core/utils/guide.dart b/lib/core/utils/guide.dart index d701a37..464c283 100644 --- a/lib/core/utils/guide.dart +++ b/lib/core/utils/guide.dart @@ -32,9 +32,9 @@ class Tulisan { ); } - static TextStyle body({Color? color}) { + static TextStyle body({Color? color, double? fontsize}) { return GoogleFonts.spaceMono( - fontSize: 16.sp, + fontSize: fontsize?.sp ?? 16.sp, fontWeight: FontWeight.w400, color: color ?? blackNavyColor, ); @@ -47,6 +47,18 @@ class Tulisan { color: color ?? blackNavyColor, ); } + + static TextStyle customText({ + Color? color, + double? fontsize, + FontWeight? fontWeight, + }) { + return GoogleFonts.spaceGrotesk( + fontSize: fontsize?.sp ?? 16.sp, + fontWeight: fontWeight ?? FontWeight.w400, + color: color ?? blackNavyColor, + ); + } } // =====================padding custom===================== diff --git a/lib/core/utils/navigation.dart b/lib/core/utils/navigation.dart index b140b57..1346db0 100644 --- a/lib/core/utils/navigation.dart +++ b/lib/core/utils/navigation.dart @@ -91,12 +91,12 @@ class _NavigationPageState extends State { items: [ BottomNavigationBarItem( icon: Icon(Iconsax.home_2), - activeIcon: Icon(Iconsax.home_2, size: 28), + activeIcon: Icon(Iconsax.home_2, size: 26), label: 'Beranda', ), BottomNavigationBarItem( icon: Icon(Iconsax.note_favorite), - activeIcon: Icon(Iconsax.note_favorite, size: 28), + activeIcon: Icon(Iconsax.note_favorite, size: 26), label: 'Aktivitas', ), const BottomNavigationBarItem( @@ -105,45 +105,61 @@ class _NavigationPageState extends State { ), BottomNavigationBarItem( icon: Icon(Iconsax.shopping_cart), - activeIcon: Icon(Iconsax.shopping_cart, size: 28), + activeIcon: Icon(Iconsax.shopping_cart, size: 26), label: 'Keranjang', ), BottomNavigationBarItem( icon: Icon(Iconsax.user), - activeIcon: Icon(Iconsax.user, size: 28), + activeIcon: Icon(Iconsax.user, size: 26), label: 'Profil', ), ], - selectedLabelStyle: const TextStyle(fontSize: 14), - unselectedLabelStyle: const TextStyle(fontSize: 12), + selectedLabelStyle: Tulisan.customText( + color: secondaryColor, + fontsize: 14, + fontWeight: FontWeight.w600, + ), + unselectedLabelStyle: Tulisan.customText( + color: whiteColor, + fontsize: 12, + fontWeight: FontWeight.w400, + ), ), ), ), ), floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, - floatingActionButton: SizedBox( + floatingActionButton: Container( width: 78, height: 78, - child: FloatingActionButton( + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + colors: [secondaryColor, primaryColor], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + border: Border.all(color: whiteColor, width: 4), + ), + child: RawMaterialButton( onPressed: () { router.push("/requestpickup"); }, - backgroundColor: primaryColor, - shape: const CircleBorder( - side: BorderSide(color: Colors.white, width: 4), - ), + shape: const CircleBorder(), elevation: 0, - highlightElevation: 0, - hoverColor: Colors.blue, - splashColor: Colors.transparent, - foregroundColor: Colors.white, child: Column( mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, children: [ - Icon(Iconsax.archive_2, color: whiteColor, size: 30), - Text("mulai", style: TextStyle(color: whiteColor)), + Icon(Iconsax.archive_2, color: Colors.white, size: 30), + Text( + "Mulai", + style: Tulisan.customText( + color: whiteColor, + fontsize: 14, + fontWeight: FontWeight.w600, + ), + ), ], ), ), diff --git a/lib/features/home/presentation/components/article_content.dart b/lib/features/home/presentation/components/article_content.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/home/presentation/components/article_list.dart b/lib/features/home/presentation/components/article_list.dart new file mode 100644 index 0000000..99e4568 --- /dev/null +++ b/lib/features/home/presentation/components/article_list.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:rijig_mobile/globaldata/article/article_vmod.dart'; + +class ArticleScreen extends StatefulWidget { + const ArticleScreen({super.key}); + + @override + State createState() => _ArticleScreenState(); +} + +class _ArticleScreenState extends State { + @override + void initState() { + super.initState(); + // Fetch data setelah frame build pertama + WidgetsBinding.instance.addPostFrameCallback((_) { + Provider.of(context, listen: false).loadArticles(); + }); + } + + @override + Widget build(BuildContext context) { + final String? baseUrl = dotenv.env["BASE_URL"]; + + return Consumer( + builder: (context, viewModel, child) { + if (viewModel.isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (viewModel.errorMessage != null) { + return Center(child: Text("Error: ${viewModel.errorMessage}")); + } + + if (viewModel.articles.isEmpty) { + return const Center(child: Text("Tidak ada artikel ditemukan.")); + } + + return ListView.builder( + itemCount: viewModel.articles.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + final article = viewModel.articles[index]; + return Card( + margin: const EdgeInsets.only(bottom: 12), + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + child: ListTile( + contentPadding: const EdgeInsets.all(12), + leading: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.network( + "$baseUrl${article.coverImage}", + width: 60, + height: 60, + fit: BoxFit.cover, + errorBuilder: + (context, error, stackTrace) => + const Icon(Icons.image_not_supported), + ), + ), + title: Text( + article.heading, + style: const TextStyle(fontWeight: FontWeight.bold), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text( + "By ${article.author} • ${article.publishedAt}", + style: const TextStyle(fontSize: 12), + ), + onTap: () { + // Navigasi ke detail (jika tersedia) + // router.push('/article-detail', extra: article.articleId); + }, + ), + ); + }, + ); + }, + ); + } +} diff --git a/lib/features/home/presentation/screen/home_screen.dart b/lib/features/home/presentation/screen/home_screen.dart index 8e501bc..b8afd2e 100644 --- a/lib/features/home/presentation/screen/home_screen.dart +++ b/lib/features/home/presentation/screen/home_screen.dart @@ -1,3 +1,5 @@ +// ignore_for_file: use_build_context_synchronously + import 'dart:math' as math; import 'package:custom_refresh_indicator/custom_refresh_indicator.dart'; @@ -7,7 +9,9 @@ import 'package:iconsax_flutter/iconsax_flutter.dart'; import 'package:provider/provider.dart'; import 'package:rijig_mobile/core/utils/guide.dart'; import 'package:rijig_mobile/features/home/presentation/components/about_comp.dart'; +import 'package:rijig_mobile/features/home/presentation/components/article_list.dart'; import 'package:rijig_mobile/features/home/presentation/viewmodel/about_vmod.dart'; +import 'package:rijig_mobile/globaldata/article/article_vmod.dart'; import 'package:rijig_mobile/widget/card_withicon.dart'; class HomeScreen extends StatefulWidget { @@ -28,6 +32,10 @@ class _HomeScreenState extends State { context, listen: false, ).getAboutList(); + await Provider.of( + context, + listen: false, + ).loadArticles(); }, backgroundColor: whiteColor, indicatorBuilder: (context, controller) { @@ -130,9 +138,11 @@ class _HomeScreenState extends State { ], ), ), + Gap(15), + ArticleScreen(), ], ), - Gap(20), + // Gap(20), ], ), ), diff --git a/lib/globaldata/article/article_model.dart b/lib/globaldata/article/article_model.dart new file mode 100644 index 0000000..16da1ae --- /dev/null +++ b/lib/globaldata/article/article_model.dart @@ -0,0 +1,34 @@ +class ArticleModel { + final String articleId; + final String title; + final String coverImage; + final String author; + final String heading; + final String content; + final String publishedAt; + final String updatedAt; + + ArticleModel({ + required this.articleId, + required this.title, + required this.coverImage, + required this.author, + required this.heading, + required this.content, + required this.publishedAt, + required this.updatedAt, + }); + + factory ArticleModel.fromJson(Map json) { + return ArticleModel( + articleId: json['article_id'], + title: json['title'], + coverImage: json['coverImage'], + author: json['author'], + heading: json['heading'], + content: json['content'], + publishedAt: json['publishedAt'], + updatedAt: json['updatedAt'], + ); + } +} diff --git a/lib/globaldata/article/article_repository.dart b/lib/globaldata/article/article_repository.dart new file mode 100644 index 0000000..7983705 --- /dev/null +++ b/lib/globaldata/article/article_repository.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; +import 'package:rijig_mobile/core/api/api_services.dart'; +import 'package:rijig_mobile/globaldata/article/article_model.dart'; + +class ArticleRepository { + final Https _https = Https(); + + Future> fetchArticles() async { + final response = await _https.get('/article-rijik/view-article'); + debugPrint("reponse article: $response"); + final List data = response['data']; + return data.map((json) => ArticleModel.fromJson(json)).toList(); + } +} diff --git a/lib/globaldata/article/article_service.dart b/lib/globaldata/article/article_service.dart new file mode 100644 index 0000000..9a070cf --- /dev/null +++ b/lib/globaldata/article/article_service.dart @@ -0,0 +1,17 @@ + +import 'package:rijig_mobile/globaldata/article/article_model.dart'; +import 'package:rijig_mobile/globaldata/article/article_repository.dart'; + +class ArticleService { + final ArticleRepository _repository; + + ArticleService(this._repository); + + Future> getAllArticles() async { + try { + return await _repository.fetchArticles(); + } catch (e) { + throw Exception('Failed to load articles: $e'); + } + } +} diff --git a/lib/globaldata/article/article_vmod.dart b/lib/globaldata/article/article_vmod.dart new file mode 100644 index 0000000..ed3cfcf --- /dev/null +++ b/lib/globaldata/article/article_vmod.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:rijig_mobile/globaldata/article/article_model.dart'; +import 'package:rijig_mobile/globaldata/article/article_service.dart'; + +class ArticleViewModel extends ChangeNotifier { + final ArticleService _articleService; + + ArticleViewModel(this._articleService); + + List _articles = []; + List get articles => _articles; + + bool isLoading = false; + String? errorMessage; + + Future loadArticles() async { + isLoading = true; + notifyListeners(); + + try { + _articles = await _articleService.getAllArticles(); + } catch (e) { + errorMessage = e.toString(); + } finally { + isLoading = false; + notifyListeners(); + } + } +} diff --git a/lib/main.dart b/lib/main.dart index 0fda871..be1dfb2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -33,6 +33,7 @@ class MyApp extends StatelessWidget { ChangeNotifierProvider(create: (_) => sl()), ChangeNotifierProvider(create: (_) => sl()), ChangeNotifierProvider(create: (_) => sl()), + ChangeNotifierProvider(create: (_) => sl()), ], child: ScreenUtilInit( designSize: const Size(375, 812),