diff --git a/lib/core/router.dart b/lib/core/router.dart index c3b5f69..2bea9aa 100644 --- a/lib/core/router.dart +++ b/lib/core/router.dart @@ -47,5 +47,13 @@ final router = GoRouter( return AboutDetailScreenComp(data: data); }, ), + + GoRoute( + path: '/artikeldetail', + builder: (context, state) { + dynamic data = state.extra; + return ArticleDetailScreen(data: data); + }, + ), ], ); diff --git a/lib/core/utils/exportimportview.dart b/lib/core/utils/exportimportview.dart index 997681b..357e0be 100644 --- a/lib/core/utils/exportimportview.dart +++ b/lib/core/utils/exportimportview.dart @@ -12,3 +12,4 @@ export 'package:rijig_mobile/features/auth/presentation/screen/verifpin_screen.d export 'package:rijig_mobile/features/launch/screen/onboardingpage_screen.dart'; export 'package:rijig_mobile/features/launch/screen/splash_screen.dart'; export 'package:rijig_mobile/features/home/presentation/components/about_detail_comp.dart'; +export 'package:rijig_mobile/features/home/presentation/components/article_content.dart'; diff --git a/lib/core/utils/navigation.dart b/lib/core/utils/navigation.dart index 1346db0..2518e9e 100644 --- a/lib/core/utils/navigation.dart +++ b/lib/core/utils/navigation.dart @@ -52,7 +52,7 @@ class _NavigationPageState extends State { Widget build(BuildContext context) { return Scaffold( extendBody: true, - backgroundColor: Colors.white, + backgroundColor: whiteColor, body: IndexedStack( index: _selectedIndex, children: const [ @@ -151,7 +151,7 @@ class _NavigationPageState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Iconsax.archive_2, color: Colors.white, size: 30), + Icon(Iconsax.archive_2, color: whiteColor, size: 30), Text( "Mulai", style: Tulisan.customText( diff --git a/lib/features/home/presentation/components/about_comp.dart b/lib/features/home/presentation/components/about_comp.dart index c6125c7..5d97f02 100644 --- a/lib/features/home/presentation/components/about_comp.dart +++ b/lib/features/home/presentation/components/about_comp.dart @@ -5,7 +5,7 @@ import 'package:provider/provider.dart'; import 'package:rijig_mobile/core/router.dart'; import 'package:rijig_mobile/core/utils/guide.dart'; import 'package:rijig_mobile/features/home/presentation/viewmodel/about_vmod.dart'; -import 'package:rijig_mobile/features/requestpick/presentation/screen/requestpickup_screen.dart'; +import 'package:rijig_mobile/widget/skeletonize.dart'; class AboutComponent extends StatefulWidget { const AboutComponent({super.key}); @@ -33,6 +33,7 @@ class AboutComponentState extends State { builder: (context, viewModel, child) { if (viewModel.isLoading) { return ListView.builder( + shrinkWrap: true, itemCount: 1, itemBuilder: (context, index) { return SkeletonCard(); @@ -99,7 +100,6 @@ class AboutComponentState extends State { ), onTap: () { debugPrint("Tapped on ${imageData['route']}"); - // imageData["route"]; router.push("/aboutdetail", extra: imageData["route"]); }, ); diff --git a/lib/features/home/presentation/components/about_detail_comp.dart b/lib/features/home/presentation/components/about_detail_comp.dart index e0aa2e2..615c978 100644 --- a/lib/features/home/presentation/components/about_detail_comp.dart +++ b/lib/features/home/presentation/components/about_detail_comp.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:provider/provider.dart'; import 'package:rijig_mobile/features/home/presentation/viewmodel/about_vmod.dart'; -import 'package:rijig_mobile/features/requestpick/presentation/screen/requestpickup_screen.dart'; import 'package:rijig_mobile/widget/appbar.dart'; +import 'package:rijig_mobile/widget/skeletonize.dart'; class AboutDetailScreenComp extends StatefulWidget { final String data; diff --git a/lib/features/home/presentation/components/article_content.dart b/lib/features/home/presentation/components/article_content.dart index e69de29..6c9cb98 100644 --- a/lib/features/home/presentation/components/article_content.dart +++ b/lib/features/home/presentation/components/article_content.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:gap/gap.dart'; +import 'package:rijig_mobile/core/utils/guide.dart'; +import 'package:rijig_mobile/globaldata/article/article_model.dart'; +import 'package:rijig_mobile/widget/appbar.dart'; + +class ArticleDetailScreen extends StatelessWidget { + final ArticleModel data; + + const ArticleDetailScreen({super.key, required this.data}); + + @override + Widget build(BuildContext context) { + final String? baseUrl = dotenv.env["BASE_URL"]; + + return Scaffold( + appBar: CustomAppBar(judul: "detail artikel"), + body: SingleChildScrollView( + padding: PaddingCustom().paddingAll(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(data.heading, style: Tulisan.heading()), + Gap(8), + Text( + "Oleh ${data.author} • ${data.publishedAt}", + style: TextStyle( + fontSize: 13.sp, + fontStyle: FontStyle.italic, + color: greyAbsolutColor, + ), + ), + Gap(30), + if (data.coverImage.isNotEmpty) + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.network( + "$baseUrl${data.coverImage}", + width: double.infinity, + fit: BoxFit.cover, + errorBuilder: + (context, error, stackTrace) => + const Icon(Icons.broken_image), + ), + ), + Gap(10), + Divider(thickness: 1.3, color: blackNavyColor), + Gap(60), + Text(data.content, style: Tulisan.customText()), + ], + ), + ), + ); + } +} diff --git a/lib/features/home/presentation/components/article_list.dart b/lib/features/home/presentation/components/article_list.dart index 99e4568..98bdb95 100644 --- a/lib/features/home/presentation/components/article_list.dart +++ b/lib/features/home/presentation/components/article_list.dart @@ -1,7 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; import 'package:provider/provider.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:rijig_mobile/core/router.dart'; +import 'package:rijig_mobile/core/utils/guide.dart'; import 'package:rijig_mobile/globaldata/article/article_vmod.dart'; +import 'package:rijig_mobile/widget/skeletonize.dart'; class ArticleScreen extends StatefulWidget { const ArticleScreen({super.key}); @@ -14,7 +18,6 @@ class _ArticleScreenState extends State { @override void initState() { super.initState(); - // Fetch data setelah frame build pertama WidgetsBinding.instance.addPostFrameCallback((_) { Provider.of(context, listen: false).loadArticles(); }); @@ -27,7 +30,13 @@ class _ArticleScreenState extends State { return Consumer( builder: (context, viewModel, child) { if (viewModel.isLoading) { - return const Center(child: CircularProgressIndicator()); + return ListView.builder( + shrinkWrap: true, + itemCount: 1, + itemBuilder: (context, index) { + return SkeletonCard(); + }, + ); } if (viewModel.errorMessage != null) { @@ -38,49 +47,78 @@ class _ArticleScreenState extends State { 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), + return SizedBox( + height: 190, + child: ListView.separated( + scrollDirection: Axis.horizontal, + clipBehavior: Clip.none, + itemCount: viewModel.articles.length, + separatorBuilder: (_, __) => Gap(12), + itemBuilder: (context, index) { + final article = viewModel.articles[index]; + return GestureDetector( + onTap: () { + router.push("/artikeldetail", extra: article); + }, + child: Container( + padding: PaddingCustom().paddingAll(3), + width: 180, + decoration: BoxDecoration( + border: Border.all(color: greyColor), + color: whiteColor, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + child: Image.network( + "$baseUrl${article.coverImage}", + width: double.infinity, + height: 100, + fit: BoxFit.cover, + errorBuilder: + (context, error, stackTrace) => + const Icon(Icons.image_not_supported), + ), + ), + Padding( + padding: PaddingCustom().paddingAll(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + article.heading, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + const SizedBox(height: 8), + Text( + "by ${article.author} • ${article.publishedAt}", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 12, + color: Colors.grey, + ), + ), + ], + ), + ), + ], ), ), - 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 b8afd2e..79dc9bc 100644 --- a/lib/features/home/presentation/screen/home_screen.dart +++ b/lib/features/home/presentation/screen/home_screen.dart @@ -122,21 +122,18 @@ class _HomeScreenState extends State { Gap(20), Column( children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text( - "Artikel", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.black, - ), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + "Artikel", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.black, ), - ], - ), + ), + ], ), Gap(15), ArticleScreen(), diff --git a/lib/features/requestpick/presentation/screen/requestpickup_screen.dart b/lib/features/requestpick/presentation/screen/requestpickup_screen.dart index 65d5586..68b183a 100644 --- a/lib/features/requestpick/presentation/screen/requestpickup_screen.dart +++ b/lib/features/requestpick/presentation/screen/requestpickup_screen.dart @@ -7,7 +7,7 @@ import 'package:provider/provider.dart'; import 'package:rijig_mobile/core/utils/guide.dart'; import 'package:rijig_mobile/globaldata/trash/trash_viewmodel.dart'; import 'package:rijig_mobile/widget/appbar.dart'; -import 'package:shimmer/shimmer.dart'; +import 'package:rijig_mobile/widget/skeletonize.dart'; class RequestPickScreen extends StatefulWidget { const RequestPickScreen({super.key}); @@ -20,7 +20,9 @@ class RequestPickScreenState extends State { @override void initState() { super.initState(); - Provider.of(context, listen: false).loadCategories(); + WidgetsBinding.instance.addPostFrameCallback((_) { + Provider.of(context, listen: false).loadCategories(); + }); } @override @@ -37,10 +39,10 @@ class RequestPickScreenState extends State { listen: false, ).loadCategories(); }, - backgroundColor: Colors.white, + backgroundColor: whiteColor, indicatorBuilder: (context, controller) { return Padding( - padding: const EdgeInsets.all(6.0), + padding: PaddingCustom().paddingAll(6), child: CircularProgressIndicator( color: primaryColor, value: @@ -54,6 +56,7 @@ class RequestPickScreenState extends State { builder: (context, viewModel, child) { if (viewModel.isLoading) { return ListView.builder( + shrinkWrap: true, itemCount: 5, itemBuilder: (context, index) { return SkeletonCard(); @@ -99,25 +102,3 @@ class RequestPickScreenState extends State { ); } } - -class SkeletonCard extends StatelessWidget { - const SkeletonCard({super.key}); - - @override - Widget build(BuildContext context) { - return Shimmer.fromColors( - baseColor: Colors.grey[300]!, - highlightColor: Colors.grey[100]!, - child: Card( - margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 15), - elevation: 4, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: ListTile( - leading: Container(width: 50, height: 50, color: Colors.white), - title: Container(width: 100, height: 15, color: Colors.white), - subtitle: Container(width: 150, height: 10, color: Colors.white), - ), - ), - ); - } -} diff --git a/lib/widget/skeletonize.dart b/lib/widget/skeletonize.dart new file mode 100644 index 0000000..89f902d --- /dev/null +++ b/lib/widget/skeletonize.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; + +class SkeletonCard extends StatelessWidget { + const SkeletonCard({super.key}); + + @override + Widget build(BuildContext context) { + return Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: Card( + margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 15), + elevation: 4, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: ListTile( + leading: Container(width: 50, height: 50, color: Colors.white), + title: Container(width: 100, height: 15, color: Colors.white), + subtitle: Container(width: 150, height: 10, color: Colors.white), + ), + ), + ); + } +} \ No newline at end of file