feat&refact: improve stle and add feature article(uncompleted)

This commit is contained in:
pahmiudahgede 2025-05-17 21:32:38 +07:00
parent 7d3f129748
commit 4f84abfeee
12 changed files with 247 additions and 22 deletions

View File

@ -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/presentation/viewmodel/about_vmod.dart';
export 'package:rijig_mobile/features/home/repositories/about_repository.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/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';

View File

@ -12,4 +12,5 @@ void init() {
); );
sl.registerFactory(() => AboutViewModel(AboutService(AboutRepository()))); sl.registerFactory(() => AboutViewModel(AboutService(AboutRepository())));
sl.registerFactory(() => AboutDetailViewModel(AboutService(AboutRepository()))); sl.registerFactory(() => AboutDetailViewModel(AboutService(AboutRepository())));
sl.registerFactory(() => ArticleViewModel(ArticleService(ArticleRepository())));
} }

View File

@ -32,9 +32,9 @@ class Tulisan {
); );
} }
static TextStyle body({Color? color}) { static TextStyle body({Color? color, double? fontsize}) {
return GoogleFonts.spaceMono( return GoogleFonts.spaceMono(
fontSize: 16.sp, fontSize: fontsize?.sp ?? 16.sp,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
color: color ?? blackNavyColor, color: color ?? blackNavyColor,
); );
@ -47,6 +47,18 @@ class Tulisan {
color: color ?? blackNavyColor, 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===================== // =====================padding custom=====================

View File

@ -91,12 +91,12 @@ class _NavigationPageState extends State<NavigationPage> {
items: [ items: [
BottomNavigationBarItem( BottomNavigationBarItem(
icon: Icon(Iconsax.home_2), icon: Icon(Iconsax.home_2),
activeIcon: Icon(Iconsax.home_2, size: 28), activeIcon: Icon(Iconsax.home_2, size: 26),
label: 'Beranda', label: 'Beranda',
), ),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: Icon(Iconsax.note_favorite), icon: Icon(Iconsax.note_favorite),
activeIcon: Icon(Iconsax.note_favorite, size: 28), activeIcon: Icon(Iconsax.note_favorite, size: 26),
label: 'Aktivitas', label: 'Aktivitas',
), ),
const BottomNavigationBarItem( const BottomNavigationBarItem(
@ -105,45 +105,61 @@ class _NavigationPageState extends State<NavigationPage> {
), ),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: Icon(Iconsax.shopping_cart), icon: Icon(Iconsax.shopping_cart),
activeIcon: Icon(Iconsax.shopping_cart, size: 28), activeIcon: Icon(Iconsax.shopping_cart, size: 26),
label: 'Keranjang', label: 'Keranjang',
), ),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: Icon(Iconsax.user), icon: Icon(Iconsax.user),
activeIcon: Icon(Iconsax.user, size: 28), activeIcon: Icon(Iconsax.user, size: 26),
label: 'Profil', label: 'Profil',
), ),
], ],
selectedLabelStyle: const TextStyle(fontSize: 14), selectedLabelStyle: Tulisan.customText(
unselectedLabelStyle: const TextStyle(fontSize: 12), color: secondaryColor,
fontsize: 14,
fontWeight: FontWeight.w600,
),
unselectedLabelStyle: Tulisan.customText(
color: whiteColor,
fontsize: 12,
fontWeight: FontWeight.w400,
),
), ),
), ),
), ),
), ),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
floatingActionButton: SizedBox( floatingActionButton: Container(
width: 78, width: 78,
height: 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: () { onPressed: () {
router.push("/requestpickup"); router.push("/requestpickup");
}, },
backgroundColor: primaryColor, shape: const CircleBorder(),
shape: const CircleBorder(
side: BorderSide(color: Colors.white, width: 4),
),
elevation: 0, elevation: 0,
highlightElevation: 0,
hoverColor: Colors.blue,
splashColor: Colors.transparent,
foregroundColor: Colors.white,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [ children: [
Icon(Iconsax.archive_2, color: whiteColor, size: 30), Icon(Iconsax.archive_2, color: Colors.white, size: 30),
Text("mulai", style: TextStyle(color: whiteColor)), Text(
"Mulai",
style: Tulisan.customText(
color: whiteColor,
fontsize: 14,
fontWeight: FontWeight.w600,
),
),
], ],
), ),
), ),

View File

@ -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<ArticleScreen> createState() => _ArticleScreenState();
}
class _ArticleScreenState extends State<ArticleScreen> {
@override
void initState() {
super.initState();
// Fetch data setelah frame build pertama
WidgetsBinding.instance.addPostFrameCallback((_) {
Provider.of<ArticleViewModel>(context, listen: false).loadArticles();
});
}
@override
Widget build(BuildContext context) {
final String? baseUrl = dotenv.env["BASE_URL"];
return Consumer<ArticleViewModel>(
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);
},
),
);
},
);
},
);
}
}

View File

@ -1,3 +1,5 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:math' as math; import 'dart:math' as math;
import 'package:custom_refresh_indicator/custom_refresh_indicator.dart'; 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:provider/provider.dart';
import 'package:rijig_mobile/core/utils/guide.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/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/features/home/presentation/viewmodel/about_vmod.dart';
import 'package:rijig_mobile/globaldata/article/article_vmod.dart';
import 'package:rijig_mobile/widget/card_withicon.dart'; import 'package:rijig_mobile/widget/card_withicon.dart';
class HomeScreen extends StatefulWidget { class HomeScreen extends StatefulWidget {
@ -28,6 +32,10 @@ class _HomeScreenState extends State<HomeScreen> {
context, context,
listen: false, listen: false,
).getAboutList(); ).getAboutList();
await Provider.of<ArticleViewModel>(
context,
listen: false,
).loadArticles();
}, },
backgroundColor: whiteColor, backgroundColor: whiteColor,
indicatorBuilder: (context, controller) { indicatorBuilder: (context, controller) {
@ -130,9 +138,11 @@ class _HomeScreenState extends State<HomeScreen> {
], ],
), ),
), ),
Gap(15),
ArticleScreen(),
], ],
), ),
Gap(20), // Gap(20),
], ],
), ),
), ),

View File

@ -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<String, dynamic> 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'],
);
}
}

View File

@ -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<List<ArticleModel>> 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();
}
}

View File

@ -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<List<ArticleModel>> getAllArticles() async {
try {
return await _repository.fetchArticles();
} catch (e) {
throw Exception('Failed to load articles: $e');
}
}
}

View File

@ -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<ArticleModel> _articles = [];
List<ArticleModel> get articles => _articles;
bool isLoading = false;
String? errorMessage;
Future<void> loadArticles() async {
isLoading = true;
notifyListeners();
try {
_articles = await _articleService.getAllArticles();
} catch (e) {
errorMessage = e.toString();
} finally {
isLoading = false;
notifyListeners();
}
}
}

View File

@ -33,6 +33,7 @@ class MyApp extends StatelessWidget {
ChangeNotifierProvider(create: (_) => sl<TrashViewModel>()), ChangeNotifierProvider(create: (_) => sl<TrashViewModel>()),
ChangeNotifierProvider(create: (_) => sl<AboutViewModel>()), ChangeNotifierProvider(create: (_) => sl<AboutViewModel>()),
ChangeNotifierProvider(create: (_) => sl<AboutDetailViewModel>()), ChangeNotifierProvider(create: (_) => sl<AboutDetailViewModel>()),
ChangeNotifierProvider(create: (_) => sl<ArticleViewModel>()),
], ],
child: ScreenUtilInit( child: ScreenUtilInit(
designSize: const Size(375, 812), designSize: const Size(375, 812),