feat: add feature get trash category

This commit is contained in:
pahmiudahgede 2025-05-15 02:36:15 +07:00
parent 9f869503b2
commit 0fbbb807d9
13 changed files with 467 additions and 15 deletions

View File

@ -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';

View File

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

View File

@ -1,3 +1,5 @@
// ignore_for_file: unrelated_type_equality_checks
import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';

View File

@ -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';

View File

@ -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<TrashViewModel>(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<TrashViewModel>(
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),
),
),
);
}
}

View File

@ -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<String, dynamic> json) {
return Category(
id: json['id'],
name: json['name'],
icon: json['icon'],
createdAt: json['createdAt'],
updatedAt: json['updatedAt'],
);
}
}
class TrashCategoryResponse {
final List<Category> categories;
final String message;
final int total;
TrashCategoryResponse({
required this.categories,
required this.message,
required this.total,
});
factory TrashCategoryResponse.fromJson(Map<String, dynamic> json) {
return TrashCategoryResponse(
categories:
(json['data'] as List).map((e) => Category.fromJson(e)).toList(),
message: json['meta']['message'],
total: json['meta']['total'],
);
}
}

View File

@ -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<TrashCategoryResponse> fetchCategories() async {
final response = await _https.get('/trash/categories');
return TrashCategoryResponse.fromJson(response);
}
}

View File

@ -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<TrashCategoryResponse> getCategories() async {
try {
return await _trashCategoryRepository.fetchCategories();
} catch (e) {
throw Exception('Failed to load categories: $e');
}
}
}

View File

@ -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<void> loadCategories() async {
isLoading = true;
errorMessage = null;
notifyListeners();
try {
trashCategoryResponse = await _trashCategoryService.getCategories();
} catch (e) {
errorMessage = "Error: ${e.toString()}";
}
isLoading = false;
notifyListeners();
}
}

View File

@ -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<LoginViewModel>()),
ChangeNotifierProvider(create: (_) => sl<OtpViewModel>()),
ChangeNotifierProvider(create: (_) => sl<LogoutViewModel>()),
ChangeNotifierProvider(create: (_) => sl<TrashViewModel>()),
],
child: ScreenUtilInit(
designSize: const Size(375, 812),

View File

@ -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<Path> {
@override
bool shouldReclip(covariant CustomClipper<Path> 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<Path> {
@override
bool shouldReclip(covariant CustomClipper<Path> 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;
}
}

View File

@ -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

View File

@ -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: