diff --git a/lib/core/container/export_vmod.dart b/lib/core/container/export_vmod.dart new file mode 100644 index 0000000..70b005b --- /dev/null +++ b/lib/core/container/export_vmod.dart @@ -0,0 +1,13 @@ +export 'package:get_it/get_it.dart'; +export 'package:rijig_mobile/features/auth/presentation/viewmodel/login_vmod.dart'; +export 'package:rijig_mobile/features/auth/presentation/viewmodel/logout_vmod.dart'; +export 'package:rijig_mobile/features/auth/presentation/viewmodel/otp_vmod.dart'; +export 'package:rijig_mobile/features/auth/repositories/login_repository.dart'; +export 'package:rijig_mobile/features/auth/repositories/logout_repository.dart'; +export 'package:rijig_mobile/features/auth/repositories/otp_repository.dart'; +export 'package:rijig_mobile/features/auth/service/login_service.dart'; +export 'package:rijig_mobile/features/auth/service/logout_service.dart'; +export 'package:rijig_mobile/features/auth/service/otp_service.dart'; +export 'package:rijig_mobile/globaldata/trash/trash_repository.dart'; +export 'package:rijig_mobile/globaldata/trash/trash_service.dart'; +export 'package:rijig_mobile/globaldata/trash/trash_viewmodel.dart'; \ No newline at end of file diff --git a/lib/core/container/injection_container.dart b/lib/core/container/injection_container.dart index 204c492..84887dd 100644 --- a/lib/core/container/injection_container.dart +++ b/lib/core/container/injection_container.dart @@ -1,13 +1,4 @@ -import 'package:get_it/get_it.dart'; -import 'package:rijig_mobile/features/auth/presentation/viewmodel/login_vmod.dart'; -import 'package:rijig_mobile/features/auth/presentation/viewmodel/logout_vmod.dart'; -import 'package:rijig_mobile/features/auth/presentation/viewmodel/otp_vmod.dart'; -import 'package:rijig_mobile/features/auth/repositories/login_repository.dart'; -import 'package:rijig_mobile/features/auth/repositories/logout_repository.dart'; -import 'package:rijig_mobile/features/auth/repositories/otp_repository.dart'; -import 'package:rijig_mobile/features/auth/service/login_service.dart'; -import 'package:rijig_mobile/features/auth/service/logout_service.dart'; -import 'package:rijig_mobile/features/auth/service/otp_service.dart'; +import 'package:rijig_mobile/core/container/export_vmod.dart'; final sl = GetIt.instance; @@ -15,4 +6,6 @@ void init() { sl.registerFactory(() => LoginViewModel(LoginService(LoginRepository()))); sl.registerFactory(() => OtpViewModel(OtpService(OtpRepository()))); sl.registerFactory(() => LogoutViewModel(LogoutService(LogoutRepository()))); + + sl.registerFactory(() => TrashViewModel(TrashCategoryService(TrashCategoryRepository()))); } diff --git a/lib/core/network/network_info.dart b/lib/core/network/network_info.dart index 995318a..673459d 100644 --- a/lib/core/network/network_info.dart +++ b/lib/core/network/network_info.dart @@ -1,3 +1,5 @@ +// ignore_for_file: unrelated_type_equality_checks + import 'dart:io'; import 'package:connectivity_plus/connectivity_plus.dart'; diff --git a/lib/features/profil/profil_screen.dart b/lib/features/profil/profil_screen.dart index d57769c..a36173e 100644 --- a/lib/features/profil/profil_screen.dart +++ b/lib/features/profil/profil_screen.dart @@ -1,3 +1,5 @@ +// ignore_for_file: use_build_context_synchronously + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:rijig_mobile/core/router.dart'; diff --git a/lib/features/requestpick/requestpickup_screen.dart b/lib/features/requestpick/requestpickup_screen.dart index e03d010..0e0e59d 100644 --- a/lib/features/requestpick/requestpickup_screen.dart +++ b/lib/features/requestpick/requestpickup_screen.dart @@ -1,14 +1,88 @@ import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:provider/provider.dart'; +import 'package:rijig_mobile/globaldata/trash/trash_viewmodel.dart'; import 'package:rijig_mobile/widget/appbar.dart'; +import 'package:shimmer/shimmer.dart'; class RequestPickScreen extends StatelessWidget { const RequestPickScreen({super.key}); @override Widget build(BuildContext context) { + Future.microtask(() { + // ignore: use_build_context_synchronously + Provider.of(context, listen: false).loadCategories(); + }); + final String? _baseUrl = dotenv.env["BASE_URL"]; + return Scaffold( - appBar: CustomAppBar(judul: "Pilih Sampah"), - body: Center(child: Text("pilih sampah anda")), + appBar: CustomAppBar(judul: "Pilih sampah"), + body: Consumer( + builder: (context, viewModel, child) { + if (viewModel.isLoading) { + return ListView.builder( + itemCount: 5, + itemBuilder: (context, index) { + return SkeletonCard(); + }, + ); + } + + if (viewModel.errorMessage != null) { + return Center(child: Text(viewModel.errorMessage!)); + } + + return ListView.builder( + itemCount: viewModel.trashCategoryResponse?.categories.length ?? 0, + itemBuilder: (context, index) { + final category = + viewModel.trashCategoryResponse!.categories[index]; + return Card( + margin: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 15, + ), + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: ListTile( + leading: Image.network( + "$_baseUrl${category.icon}", + width: 50, + height: 50, + fit: BoxFit.cover, + ), + title: Text(category.name), + ), + ); + }, + ); + }, + ), + ); + } +} + +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/globaldata/trash/trash_model.dart b/lib/globaldata/trash/trash_model.dart new file mode 100644 index 0000000..cadf017 --- /dev/null +++ b/lib/globaldata/trash/trash_model.dart @@ -0,0 +1,46 @@ +class Category { + final String id; + final String name; + final String icon; + final String createdAt; + final String updatedAt; + + Category({ + required this.id, + required this.name, + required this.icon, + required this.createdAt, + required this.updatedAt, + }); + + factory Category.fromJson(Map json) { + return Category( + id: json['id'], + name: json['name'], + icon: json['icon'], + createdAt: json['createdAt'], + updatedAt: json['updatedAt'], + ); + } +} + +class TrashCategoryResponse { + final List categories; + final String message; + final int total; + + TrashCategoryResponse({ + required this.categories, + required this.message, + required this.total, + }); + + factory TrashCategoryResponse.fromJson(Map json) { + return TrashCategoryResponse( + categories: + (json['data'] as List).map((e) => Category.fromJson(e)).toList(), + message: json['meta']['message'], + total: json['meta']['total'], + ); + } +} diff --git a/lib/globaldata/trash/trash_repository.dart b/lib/globaldata/trash/trash_repository.dart new file mode 100644 index 0000000..7d8c890 --- /dev/null +++ b/lib/globaldata/trash/trash_repository.dart @@ -0,0 +1,11 @@ +import 'package:rijig_mobile/core/api/api_services.dart'; +import 'package:rijig_mobile/globaldata/trash/trash_model.dart'; + +class TrashCategoryRepository { + final Https _https = Https(); + + Future fetchCategories() async { + final response = await _https.get('/trash/categories'); + return TrashCategoryResponse.fromJson(response); + } +} diff --git a/lib/globaldata/trash/trash_service.dart b/lib/globaldata/trash/trash_service.dart new file mode 100644 index 0000000..f2f6a24 --- /dev/null +++ b/lib/globaldata/trash/trash_service.dart @@ -0,0 +1,16 @@ +import 'package:rijig_mobile/globaldata/trash/trash_model.dart'; +import 'package:rijig_mobile/globaldata/trash/trash_repository.dart'; + +class TrashCategoryService { + final TrashCategoryRepository _trashCategoryRepository; + + TrashCategoryService(this._trashCategoryRepository); + + Future getCategories() async { + try { + return await _trashCategoryRepository.fetchCategories(); + } catch (e) { + throw Exception('Failed to load categories: $e'); + } + } +} diff --git a/lib/globaldata/trash/trash_viewmodel.dart b/lib/globaldata/trash/trash_viewmodel.dart new file mode 100644 index 0000000..b7f5265 --- /dev/null +++ b/lib/globaldata/trash/trash_viewmodel.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:rijig_mobile/globaldata/trash/trash_model.dart'; +import 'package:rijig_mobile/globaldata/trash/trash_service.dart'; + +class TrashViewModel extends ChangeNotifier { + final TrashCategoryService _trashCategoryService; + + TrashViewModel(this._trashCategoryService); + + bool isLoading = false; + String? errorMessage; + TrashCategoryResponse? trashCategoryResponse; + + Future loadCategories() async { + isLoading = true; + errorMessage = null; + notifyListeners(); + + try { + trashCategoryResponse = await _trashCategoryService.getCategories(); + } catch (e) { + errorMessage = "Error: ${e.toString()}"; + } + + isLoading = false; + notifyListeners(); + } +} diff --git a/lib/main.dart b/lib/main.dart index 5c5bcb6..2ca682f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,9 +7,7 @@ import 'package:intl/date_symbol_data_local.dart'; import 'package:provider/provider.dart'; import 'package:rijig_mobile/core/container/injection_container.dart'; import 'package:rijig_mobile/core/router.dart'; -import 'package:rijig_mobile/features/auth/presentation/viewmodel/login_vmod.dart'; -import 'package:rijig_mobile/features/auth/presentation/viewmodel/logout_vmod.dart'; -import 'package:rijig_mobile/features/auth/presentation/viewmodel/otp_vmod.dart'; +import 'package:rijig_mobile/core/container/export_vmod.dart'; void main() async { await dotenv.load(fileName: "server/.env.dev"); @@ -31,6 +29,8 @@ class MyApp extends StatelessWidget { ChangeNotifierProvider(create: (_) => sl()), ChangeNotifierProvider(create: (_) => sl()), ChangeNotifierProvider(create: (_) => sl()), + + ChangeNotifierProvider(create: (_) => sl()), ], child: ScreenUtilInit( designSize: const Size(375, 812), diff --git a/lib/widget/card_withicon_two.dart b/lib/widget/card_withicon_two.dart new file mode 100644 index 0000000..db6703d --- /dev/null +++ b/lib/widget/card_withicon_two.dart @@ -0,0 +1,258 @@ +import 'package:flutter/material.dart'; + +class PaymentItem extends StatelessWidget { + final String title; + final String? category; + final IconData iconData; + final String amount; + + const PaymentItem( + {super.key, + required this.title, + this.category, + required this.iconData, + required this.amount}); + + @override + Widget build(BuildContext context) { + return Container( + height: 100, + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(20)), + color: const Color(0xFF2c3135), + boxShadow: [ + BoxShadow( + color: Colors.white.withOpacity(0.05), + offset: const Offset(-10, -10), + spreadRadius: 0, + blurRadius: 10), + BoxShadow( + color: Colors.black87.withOpacity(0.3), + offset: const Offset(10, 10), + spreadRadius: 0, + blurRadius: 10) + ]), + child: Row( + children: [ + SizedBox( + height: 60, + width: 60, + child: NeumorphicCircle( + innerShadow: false, + outerShadow: true, + backgroundColor: const Color(0xFF2c3135), + shadowColor: Colors.black87, + highlightColor: Colors.white.withOpacity(0.05), + child: Icon( + iconData, + size: 28, + color: Colors.white, + ), + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + title, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w600), + ), + const SizedBox( + height: 4, + ), + (category == null) + ? const SizedBox.shrink() + : Text(category!, + style: TextStyle( + color: Colors.yellow.shade200.withOpacity(0.7), + fontSize: 16, + fontWeight: FontWeight.w600)) + ], + )), + Text(amount, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w600)) + ], + ), + ); + } +} + +class NeumorphicCircle extends StatelessWidget { + final bool innerShadow; + final bool outerShadow; + final Color highlightColor; + final Color shadowColor; + final Color backgroundColor; + final Widget? child; + + const NeumorphicCircle( + {super.key, + required this.innerShadow, + required this.outerShadow, + required this.highlightColor, + required this.shadowColor, + required this.backgroundColor, + this.child}); + + @override + Widget build(BuildContext context) { + return Stack(alignment: Alignment.center, children: [ + Container( + decoration: BoxDecoration( + color: backgroundColor, + shape: BoxShape.circle, + boxShadow: (outerShadow) + ? [ + BoxShadow( + color: highlightColor, + offset: const Offset(-10, -10), + blurRadius: 20, + spreadRadius: 0), + BoxShadow( + color: shadowColor, + offset: const Offset(10, 10), + blurRadius: 20, + spreadRadius: 0) + ] + : null)), + (innerShadow) + ? ClipPath( + clipper: HighlightClipper(), + child: CircleInnerHighlight( + highlightColor: highlightColor, + backgroundColor: backgroundColor, + )) + : const SizedBox.shrink(), + (innerShadow) + ? ClipPath( + clipper: ShadowClipper(), + child: CircleInnerShadow( + shadowColor: shadowColor, + backgroundColor: backgroundColor, + ), + ) + : const SizedBox.shrink(), + (child != null) ? child! : const SizedBox.shrink() + ]); + } +} + +class CircleInnerShadow extends StatelessWidget { + final Color shadowColor; + final Color backgroundColor; + + const CircleInnerShadow( + {super.key, required this.shadowColor, required this.backgroundColor}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [ + backgroundColor, + shadowColor, + ], + center: const AlignmentDirectional(0.05, 0.05), + focal: const AlignmentDirectional(0, 0), + radius: 0.5, + focalRadius: 0, + stops: const [0.75, 1.0], + ), + ), + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + stops: const [0, 0.45], + colors: [backgroundColor.withOpacity(0), backgroundColor])), + ), + ); + } +} + +class CircleInnerHighlight extends StatelessWidget { + final Color highlightColor; + final Color backgroundColor; + + const CircleInnerHighlight( + {super.key, required this.highlightColor, required this.backgroundColor}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [ + backgroundColor, + highlightColor, + ], + center: const AlignmentDirectional(-0.05, -0.05), + focal: const AlignmentDirectional(-0.05, -0.05), + radius: 0.6, + focalRadius: 0.1, + stops: const [0.75, 1.0], + ), + ), + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + stops: const [0.55, 1], + colors: [backgroundColor, backgroundColor.withOpacity(0)])), + ), + ); + } +} + +class ShadowClipper extends CustomClipper { + @override + bool shouldReclip(covariant CustomClipper oldClipper) { + return true; + } + + @override + Path getClip(Size size) { + Path path = Path(); + path.moveTo(0, 0); + path.lineTo(size.width, 0); + path.lineTo(0, size.height); + path.close(); + return path; + } +} + +class HighlightClipper extends CustomClipper { + @override + bool shouldReclip(covariant CustomClipper oldClipper) { + return true; + } + + @override + Path getClip(Size size) { + Path path = Path(); + path.moveTo(size.width, 0); + path.lineTo(0, size.height); + path.lineTo(size.width, size.height); + path.close(); + return path; + } +} diff --git a/pubspec.lock b/pubspec.lock index e1d730c..7d15b36 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -560,6 +560,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.1" + shimmer: + dependency: "direct main" + description: + name: shimmer + sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" + url: "https://pub.dev" + source: hosted + version: "3.0.0" sky_engine: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 33ca4ed..c18f374 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,6 +29,7 @@ dependencies: pin_code_fields: ^8.0.1 provider: ^6.1.4 shared_preferences: ^2.3.3 + shimmer: ^3.0.0 uuid: ^4.5.1 dev_dependencies: