product and category loader
This commit is contained in:
parent
7bcf54c555
commit
754048b565
@ -4,6 +4,7 @@ import 'dart:developer';
|
|||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:dartz/dartz.dart';
|
import 'package:dartz/dartz.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:injectable/injectable.dart';
|
||||||
|
|
||||||
import '../../../domain/category/category.dart';
|
import '../../../domain/category/category.dart';
|
||||||
|
|
||||||
@ -11,6 +12,7 @@ part 'category_loader_event.dart';
|
|||||||
part 'category_loader_state.dart';
|
part 'category_loader_state.dart';
|
||||||
part 'category_loader_bloc.freezed.dart';
|
part 'category_loader_bloc.freezed.dart';
|
||||||
|
|
||||||
|
@injectable
|
||||||
class CategoryLoaderBloc
|
class CategoryLoaderBloc
|
||||||
extends Bloc<CategoryLoaderEvent, CategoryLoaderState> {
|
extends Bloc<CategoryLoaderEvent, CategoryLoaderState> {
|
||||||
final ICategoryRepository _categoryRepository;
|
final ICategoryRepository _categoryRepository;
|
||||||
|
|||||||
15
lib/common/extension/double_extension.dart
Normal file
15
lib/common/extension/double_extension.dart
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
part of 'extension.dart';
|
||||||
|
|
||||||
|
extension DoubleExt on double {
|
||||||
|
String get currencyFormatRp => NumberFormat.currency(
|
||||||
|
locale: 'id',
|
||||||
|
symbol: 'Rp. ',
|
||||||
|
decimalDigits: 0,
|
||||||
|
).format(this);
|
||||||
|
|
||||||
|
String get currencyFormatRpV2 => NumberFormat.currency(
|
||||||
|
locale: 'id',
|
||||||
|
symbol: 'Rp ',
|
||||||
|
decimalDigits: 0,
|
||||||
|
).format(this);
|
||||||
|
}
|
||||||
@ -1,3 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
part 'build_context_extension.dart';
|
part 'build_context_extension.dart';
|
||||||
|
part 'int_extension.dart';
|
||||||
|
part 'double_extension.dart';
|
||||||
|
|||||||
15
lib/common/extension/int_extension.dart
Normal file
15
lib/common/extension/int_extension.dart
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
part of 'extension.dart';
|
||||||
|
|
||||||
|
extension IntegerExt on int {
|
||||||
|
String get currencyFormatRp => NumberFormat.currency(
|
||||||
|
locale: 'id',
|
||||||
|
symbol: 'Rp. ',
|
||||||
|
decimalDigits: 0,
|
||||||
|
).format(this);
|
||||||
|
|
||||||
|
String get currencyFormatRpV2 => NumberFormat.currency(
|
||||||
|
locale: 'id',
|
||||||
|
symbol: 'Rp ',
|
||||||
|
decimalDigits: 0,
|
||||||
|
).format(this);
|
||||||
|
}
|
||||||
@ -12,6 +12,8 @@
|
|||||||
import 'package:apskel_pos_flutter_v2/application/auth/auth_bloc.dart' as _i343;
|
import 'package:apskel_pos_flutter_v2/application/auth/auth_bloc.dart' as _i343;
|
||||||
import 'package:apskel_pos_flutter_v2/application/auth/login_form/login_form_bloc.dart'
|
import 'package:apskel_pos_flutter_v2/application/auth/login_form/login_form_bloc.dart'
|
||||||
as _i46;
|
as _i46;
|
||||||
|
import 'package:apskel_pos_flutter_v2/application/category/category_loader/category_loader_bloc.dart'
|
||||||
|
as _i1018;
|
||||||
import 'package:apskel_pos_flutter_v2/application/outlet/outlet_loader/outlet_loader_bloc.dart'
|
import 'package:apskel_pos_flutter_v2/application/outlet/outlet_loader/outlet_loader_bloc.dart'
|
||||||
as _i76;
|
as _i76;
|
||||||
import 'package:apskel_pos_flutter_v2/application/product/product_loader/product_loader_bloc.dart'
|
import 'package:apskel_pos_flutter_v2/application/product/product_loader/product_loader_bloc.dart'
|
||||||
@ -147,6 +149,9 @@ extension GetItInjectableX on _i174.GetIt {
|
|||||||
gh.factory<_i46.LoginFormBloc>(
|
gh.factory<_i46.LoginFormBloc>(
|
||||||
() => _i46.LoginFormBloc(gh<_i776.IAuthRepository>()),
|
() => _i46.LoginFormBloc(gh<_i776.IAuthRepository>()),
|
||||||
);
|
);
|
||||||
|
gh.factory<_i1018.CategoryLoaderBloc>(
|
||||||
|
() => _i1018.CategoryLoaderBloc(gh<_i502.ICategoryRepository>()),
|
||||||
|
);
|
||||||
gh.factory<_i343.AuthBloc>(
|
gh.factory<_i343.AuthBloc>(
|
||||||
() => _i343.AuthBloc(
|
() => _i343.AuthBloc(
|
||||||
gh<_i776.IAuthRepository>(),
|
gh<_i776.IAuthRepository>(),
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import '../application/auth/auth_bloc.dart';
|
import '../application/auth/auth_bloc.dart';
|
||||||
import '../application/category/category_loader/category_loader_bloc.dart';
|
import '../application/category/category_loader/category_loader_bloc.dart';
|
||||||
import '../application/outlet/outlet_loader/outlet_loader_bloc.dart';
|
import '../application/outlet/outlet_loader/outlet_loader_bloc.dart';
|
||||||
|
import '../application/product/product_loader/product_loader_bloc.dart';
|
||||||
import '../common/theme/theme.dart';
|
import '../common/theme/theme.dart';
|
||||||
import '../common/constant/app_constant.dart';
|
import '../common/constant/app_constant.dart';
|
||||||
import '../injection.dart';
|
import '../injection.dart';
|
||||||
@ -27,6 +28,7 @@ class _AppWidgetState extends State<AppWidget> {
|
|||||||
BlocProvider(create: (context) => getIt<AuthBloc>()),
|
BlocProvider(create: (context) => getIt<AuthBloc>()),
|
||||||
BlocProvider(create: (context) => getIt<OutletLoaderBloc>()),
|
BlocProvider(create: (context) => getIt<OutletLoaderBloc>()),
|
||||||
BlocProvider(create: (context) => getIt<CategoryLoaderBloc>()),
|
BlocProvider(create: (context) => getIt<CategoryLoaderBloc>()),
|
||||||
|
BlocProvider(create: (context) => getIt<ProductLoaderBloc>()),
|
||||||
],
|
],
|
||||||
child: MaterialApp.router(
|
child: MaterialApp.router(
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
|
|||||||
47
lib/presentation/components/card/error_card.dart
Normal file
47
lib/presentation/components/card/error_card.dart
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../button/button.dart';
|
||||||
|
|
||||||
|
class ErrorCard extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
final String message;
|
||||||
|
final Function() onTap;
|
||||||
|
const ErrorCard({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
required this.message,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.error_outline, size: 48, color: Colors.red.shade400),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 32),
|
||||||
|
child: Text(
|
||||||
|
message,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(color: Colors.grey.shade600),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
AppElevatedButton.filled(
|
||||||
|
width: 120,
|
||||||
|
onPressed: onTap,
|
||||||
|
label: 'Coba Lagi',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
110
lib/presentation/components/card/product_card.dart
Normal file
110
lib/presentation/components/card/product_card.dart
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../../common/extension/extension.dart';
|
||||||
|
import '../../../common/theme/theme.dart';
|
||||||
|
import '../../../domain/product/product.dart';
|
||||||
|
import '../image/image.dart';
|
||||||
|
import '../spaces/space.dart';
|
||||||
|
|
||||||
|
class ProductCard extends StatelessWidget {
|
||||||
|
final Product product;
|
||||||
|
const ProductCard({super.key, required this.product});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {},
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColor.white,
|
||||||
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
|
border: Border.all(color: AppColor.disabled),
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(4.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(8.0)),
|
||||||
|
child: product.imageUrl == ""
|
||||||
|
? Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 120,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColor.disabled.withOpacity(0.4),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.image,
|
||||||
|
color: AppColor.textSecondary,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: AppNetworkImage(
|
||||||
|
url: product.imageUrl,
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
width: double.infinity,
|
||||||
|
height: 140,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Text(
|
||||||
|
product.name,
|
||||||
|
style: AppStyle.md.copyWith(fontWeight: FontWeight.w700),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
SpaceHeight(4),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(
|
||||||
|
product.price.currencyFormatRp,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SpaceHeight(4),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
Positioned(
|
||||||
|
top: 4,
|
||||||
|
right: 4,
|
||||||
|
child: Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
padding: const EdgeInsets.all(6),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(9.0)),
|
||||||
|
color: AppColor.primary,
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
0.toString(),
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (product.isActive == false)
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(8.0)),
|
||||||
|
color: AppColor.disabled.withOpacity(0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
8
lib/presentation/components/image/image.dart
Normal file
8
lib/presentation/components/image/image.dart
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:shimmer/shimmer.dart';
|
||||||
|
|
||||||
|
import '../../../common/theme/theme.dart';
|
||||||
|
|
||||||
|
part 'network_image.dart';
|
||||||
77
lib/presentation/components/image/network_image.dart
Normal file
77
lib/presentation/components/image/network_image.dart
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
part of 'image.dart';
|
||||||
|
|
||||||
|
class AppNetworkImage extends StatelessWidget {
|
||||||
|
final String? url;
|
||||||
|
final double? height;
|
||||||
|
final double? width;
|
||||||
|
final double? borderRadius;
|
||||||
|
final BoxFit? fit;
|
||||||
|
final bool? isCanZoom;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
|
const AppNetworkImage({
|
||||||
|
super.key,
|
||||||
|
this.url,
|
||||||
|
this.height,
|
||||||
|
this.width,
|
||||||
|
this.borderRadius = 0,
|
||||||
|
this.fit = BoxFit.cover,
|
||||||
|
this.isCanZoom = false,
|
||||||
|
this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Widget customPhoto(
|
||||||
|
double? heightx,
|
||||||
|
double? widthx,
|
||||||
|
BoxFit? fitx,
|
||||||
|
double? radius,
|
||||||
|
) {
|
||||||
|
return CachedNetworkImage(
|
||||||
|
imageUrl: url.toString(),
|
||||||
|
memCacheHeight: 120,
|
||||||
|
memCacheWidth: 120,
|
||||||
|
placeholder: (context, url) => Shimmer.fromColors(
|
||||||
|
baseColor: Colors.grey[300]!,
|
||||||
|
highlightColor: Colors.grey[100]!,
|
||||||
|
child: Container(
|
||||||
|
height: height,
|
||||||
|
width: width,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
borderRadius: BorderRadius.circular(radius ?? 0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
errorWidget: (context, url, error) {
|
||||||
|
FirebaseCrashlytics.instance.recordError(
|
||||||
|
error,
|
||||||
|
StackTrace.current,
|
||||||
|
reason: 'Failed to load image from: $url',
|
||||||
|
fatal: false,
|
||||||
|
);
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 120,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColor.disabled.withOpacity(0.4),
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.image, color: AppColor.disabled),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
height: heightx,
|
||||||
|
width: widthx,
|
||||||
|
fit: fitx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(borderRadius!),
|
||||||
|
child: customPhoto(height, width, BoxFit.fill, borderRadius),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,12 +1,16 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
import '../../../../../application/category/category_loader/category_loader_bloc.dart';
|
||||||
|
import '../../../../../application/product/product_loader/product_loader_bloc.dart';
|
||||||
import '../../../../../common/theme/theme.dart';
|
import '../../../../../common/theme/theme.dart';
|
||||||
|
import '../../../../../injection.dart';
|
||||||
import 'widgets/home_left_panel.dart';
|
import 'widgets/home_left_panel.dart';
|
||||||
import 'widgets/home_right_panel.dart';
|
import 'widgets/home_right_panel.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class HomePage extends StatelessWidget {
|
class HomePage extends StatelessWidget implements AutoRouteWrapper {
|
||||||
const HomePage({super.key});
|
const HomePage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -30,4 +34,20 @@ class HomePage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget wrappedRoute(BuildContext context) => MultiBlocProvider(
|
||||||
|
providers: [
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) =>
|
||||||
|
getIt<CategoryLoaderBloc>()
|
||||||
|
..add(CategoryLoaderEvent.getCategories()),
|
||||||
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) =>
|
||||||
|
getIt<ProductLoaderBloc>()..add(ProductLoaderEvent.getProduct()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: this,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,129 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
import '../../../../../../application/product/product_loader/product_loader_bloc.dart';
|
||||||
|
import '../../../../../../common/theme/theme.dart';
|
||||||
|
import '../../../../../../domain/category/category.dart';
|
||||||
|
|
||||||
|
class CategoryTabBar extends StatefulWidget {
|
||||||
|
final List<Category> categories;
|
||||||
|
final List<Widget> tabViews;
|
||||||
|
const CategoryTabBar({
|
||||||
|
super.key,
|
||||||
|
required this.categories,
|
||||||
|
required this.tabViews,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CategoryTabBar> createState() => _CategoryTabBarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CategoryTabBarState extends State<CategoryTabBar>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late TabController _tabController;
|
||||||
|
String? selectedCategoryId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_tabController = TabController(
|
||||||
|
length: widget.categories.length,
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
|
||||||
|
_tabController.addListener(() {
|
||||||
|
if (_tabController.indexIsChanging) {
|
||||||
|
if (_tabController.index == 0) {
|
||||||
|
context.read<ProductLoaderBloc>().add(
|
||||||
|
ProductLoaderEvent.getProduct(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
selectedCategoryId = widget.categories[_tabController.index].id;
|
||||||
|
context.read<ProductLoaderBloc>().add(
|
||||||
|
ProductLoaderEvent.getProduct(categoryId: selectedCategoryId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(CategoryTabBar oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
|
||||||
|
// ✅ Update TabController when categories length changes
|
||||||
|
if (oldWidget.categories.length != widget.categories.length) {
|
||||||
|
_tabController.dispose();
|
||||||
|
_tabController = TabController(
|
||||||
|
length: widget.categories.length,
|
||||||
|
vsync: this,
|
||||||
|
initialIndex: 0, // Reset to first tab
|
||||||
|
);
|
||||||
|
|
||||||
|
_tabController.addListener(() {
|
||||||
|
if (_tabController.indexIsChanging) {
|
||||||
|
if (_tabController.index == 0) {
|
||||||
|
context.read<ProductLoaderBloc>().add(
|
||||||
|
ProductLoaderEvent.getProduct(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
selectedCategoryId = widget.categories[_tabController.index].id;
|
||||||
|
context.read<ProductLoaderBloc>().add(
|
||||||
|
ProductLoaderEvent.getProduct(categoryId: selectedCategoryId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_tabController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Material(
|
||||||
|
elevation: 0,
|
||||||
|
color: Colors.white,
|
||||||
|
borderOnForeground: false,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
child: TabBar(
|
||||||
|
controller: _tabController,
|
||||||
|
isScrollable: true,
|
||||||
|
tabAlignment: TabAlignment.start,
|
||||||
|
labelColor: AppColor.primary,
|
||||||
|
labelStyle: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
dividerColor: AppColor.primary,
|
||||||
|
unselectedLabelColor: AppColor.primary,
|
||||||
|
indicatorSize: TabBarIndicatorSize.label,
|
||||||
|
indicatorWeight: 4,
|
||||||
|
indicatorColor: AppColor.primary,
|
||||||
|
tabs: widget.categories
|
||||||
|
.map(
|
||||||
|
(category) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Tab(text: category.name),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
// ✅ ini bagian penting
|
||||||
|
child: TabBarView(
|
||||||
|
controller: _tabController,
|
||||||
|
children: widget.tabViews,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,15 +1,214 @@
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'dart:developer';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flutter_spinkit/flutter_spinkit.dart';
|
||||||
|
|
||||||
|
import '../../../../../../application/category/category_loader/category_loader_bloc.dart';
|
||||||
|
import '../../../../../../application/product/product_loader/product_loader_bloc.dart';
|
||||||
|
import '../../../../../../common/theme/theme.dart';
|
||||||
|
import '../../../../../../domain/category/category.dart';
|
||||||
|
import '../../../../../components/card/error_card.dart';
|
||||||
|
import '../../../../../components/card/product_card.dart';
|
||||||
|
import '../../../../../components/loader/loader_with_text.dart';
|
||||||
|
import 'category_tabbar.dart';
|
||||||
import 'home_title.dart';
|
import 'home_title.dart';
|
||||||
|
|
||||||
class HomeLeftPanel extends StatelessWidget {
|
class HomeLeftPanel extends StatefulWidget {
|
||||||
const HomeLeftPanel({super.key});
|
const HomeLeftPanel({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<HomeLeftPanel> createState() => _HomeLeftPanelState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HomeLeftPanelState extends State<HomeLeftPanel> {
|
||||||
|
final searchController = TextEditingController();
|
||||||
|
final ScrollController scrollController = ScrollController();
|
||||||
|
String searchQuery = '';
|
||||||
|
|
||||||
|
bool _handleScrollNotification(
|
||||||
|
ScrollNotification notification,
|
||||||
|
String? categoryId,
|
||||||
|
) {
|
||||||
|
if (notification is ScrollEndNotification &&
|
||||||
|
scrollController.position.extentAfter == 0) {
|
||||||
|
log('📄 Loading more products...');
|
||||||
|
context.read<ProductLoaderBloc>().add(ProductLoaderEvent.loadMore());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<CategoryLoaderBloc, CategoryLoaderState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
if (state.isLoadingMore) {
|
||||||
|
return Center(child: LoaderWithText());
|
||||||
|
}
|
||||||
|
return state.failureOptionCategory.fold(
|
||||||
|
() => _buildContent(context, state.categories),
|
||||||
|
(f) => f.maybeMap(
|
||||||
|
orElse: () => ErrorCard(
|
||||||
|
title: 'Error Kategori',
|
||||||
|
message: 'Terjadi kesalahan saat memuat kategori',
|
||||||
|
onTap: () => context.read<CategoryLoaderBloc>().add(
|
||||||
|
CategoryLoaderEvent.getCategories(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
dynamicErrorMessage: (value) => ErrorCard(
|
||||||
|
title: 'Error Kategori',
|
||||||
|
message: value.erroMessage,
|
||||||
|
onTap: () => context.read<CategoryLoaderBloc>().add(
|
||||||
|
CategoryLoaderEvent.getCategories(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
localStorageError: (value) => ErrorCard(
|
||||||
|
title: 'Error Kategori',
|
||||||
|
message: value.erroMessage,
|
||||||
|
onTap: () => context.read<CategoryLoaderBloc>().add(
|
||||||
|
CategoryLoaderEvent.getCategories(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
serverError: (value) => ErrorCard(
|
||||||
|
title: 'Error Kategori',
|
||||||
|
message: 'Terjadi kesalahan saat memuat kategori',
|
||||||
|
onTap: () => context.read<CategoryLoaderBloc>().add(
|
||||||
|
CategoryLoaderEvent.getCategories(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
unexpectedError: (value) => ErrorCard(
|
||||||
|
title: 'Error Kategori',
|
||||||
|
message: 'Terjadi kesalahan saat memuat kategori',
|
||||||
|
onTap: () => context.read<CategoryLoaderBloc>().add(
|
||||||
|
CategoryLoaderEvent.getCategories(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
empty: (value) => ErrorCard(
|
||||||
|
title: 'Error Kategori',
|
||||||
|
message: 'Kategori kosong',
|
||||||
|
onTap: () => context.read<CategoryLoaderBloc>().add(
|
||||||
|
CategoryLoaderEvent.getCategories(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Column _buildContent(BuildContext context, List<Category> categories) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
HomeTitle(controller: TextEditingController(), onChanged: (value) {}),
|
HomeTitle(
|
||||||
|
controller: searchController,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
searchQuery = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fast local search
|
||||||
|
Future.delayed(Duration(milliseconds: 200), () {
|
||||||
|
if (value == searchController.text) {
|
||||||
|
log('🔍 Local search: "$value"');
|
||||||
|
context.read<ProductLoaderBloc>().add(
|
||||||
|
ProductLoaderEvent.searchProduct(query: value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: BlocBuilder<ProductLoaderBloc, ProductLoaderState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return CategoryTabBar(
|
||||||
|
key: ValueKey(categories.length),
|
||||||
|
categories: categories,
|
||||||
|
tabViews: categories.map((category) {
|
||||||
|
return SizedBox(
|
||||||
|
child: state.failureOptionProduct.fold(
|
||||||
|
() => _buildProductGrid(
|
||||||
|
state.products,
|
||||||
|
state.hasReachedMax,
|
||||||
|
state.isLoadingMore,
|
||||||
|
state.categoryId,
|
||||||
|
state.page,
|
||||||
|
),
|
||||||
|
(f) => f.maybeMap(
|
||||||
|
orElse: () => ErrorCard(
|
||||||
|
title: 'Error Produk',
|
||||||
|
message: 'Terjadi kesalahan saat memuat produk',
|
||||||
|
onTap: () => context.read<ProductLoaderBloc>().add(
|
||||||
|
ProductLoaderEvent.getProduct(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildProductGrid(
|
||||||
|
List products,
|
||||||
|
bool hasReachedMax,
|
||||||
|
bool isLoadingMore,
|
||||||
|
String? categoryId,
|
||||||
|
int currentPage,
|
||||||
|
) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
if (products.isNotEmpty)
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 0,
|
||||||
|
).copyWith(bottom: 6),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'${products.length} produk ditemukan',
|
||||||
|
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
|
||||||
|
),
|
||||||
|
Spacer(),
|
||||||
|
if (isLoadingMore)
|
||||||
|
SizedBox(
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
child: SpinKitFadingCircle(
|
||||||
|
color: AppColor.primary,
|
||||||
|
size: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
Expanded(
|
||||||
|
child: NotificationListener<ScrollNotification>(
|
||||||
|
onNotification: (notification) =>
|
||||||
|
_handleScrollNotification(notification, categoryId),
|
||||||
|
child: GridView.builder(
|
||||||
|
itemCount: products.length,
|
||||||
|
controller: scrollController,
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
cacheExtent: 200.0,
|
||||||
|
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
|
maxCrossAxisExtent: 180,
|
||||||
|
mainAxisSpacing: 30,
|
||||||
|
crossAxisSpacing: 30,
|
||||||
|
childAspectRatio: 180 / 240,
|
||||||
|
),
|
||||||
|
itemBuilder: (context, index) =>
|
||||||
|
ProductCard(product: products[index]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,7 +56,7 @@ class HomeRoute extends _i10.PageRouteInfo<void> {
|
|||||||
static _i10.PageInfo page = _i10.PageInfo(
|
static _i10.PageInfo page = _i10.PageInfo(
|
||||||
name,
|
name,
|
||||||
builder: (data) {
|
builder: (data) {
|
||||||
return const _i2.HomePage();
|
return _i10.WrappedRoute(child: const _i2.HomePage());
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -152,7 +152,7 @@ class SyncRoute extends _i10.PageRouteInfo<void> {
|
|||||||
static _i10.PageInfo page = _i10.PageInfo(
|
static _i10.PageInfo page = _i10.PageInfo(
|
||||||
name,
|
name,
|
||||||
builder: (data) {
|
builder: (data) {
|
||||||
return const _i8.SyncPage();
|
return _i10.WrappedRoute(child: const _i8.SyncPage());
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
72
pubspec.lock
72
pubspec.lock
@ -161,6 +161,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.12.0"
|
version: "8.12.0"
|
||||||
|
cached_network_image:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: cached_network_image
|
||||||
|
sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.4.1"
|
||||||
|
cached_network_image_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cached_network_image_platform_interface
|
||||||
|
sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.1"
|
||||||
|
cached_network_image_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cached_network_image_web
|
||||||
|
sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.1"
|
||||||
characters:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -406,6 +430,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "9.1.1"
|
version: "9.1.1"
|
||||||
|
flutter_cache_manager:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_cache_manager
|
||||||
|
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.4.1"
|
||||||
flutter_gen_core:
|
flutter_gen_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -704,6 +736,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.0"
|
version: "0.5.0"
|
||||||
|
octo_image:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: octo_image
|
||||||
|
sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.0"
|
||||||
package_config:
|
package_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -848,6 +888,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.0"
|
version: "4.1.0"
|
||||||
|
rxdart:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: rxdart
|
||||||
|
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.28.0"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -920,6 +968,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "3.0.0"
|
||||||
|
shimmer:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: shimmer
|
||||||
|
sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -949,6 +1005,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.1"
|
version: "1.10.1"
|
||||||
|
sprintf:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sprintf
|
||||||
|
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.0"
|
||||||
sqflite:
|
sqflite:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1069,6 +1133,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.0"
|
||||||
|
uuid:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: uuid
|
||||||
|
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.5.1"
|
||||||
vector_graphics:
|
vector_graphics:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -34,6 +34,8 @@ dependencies:
|
|||||||
bloc: ^9.1.0
|
bloc: ^9.1.0
|
||||||
flutter_bloc: ^9.1.1
|
flutter_bloc: ^9.1.1
|
||||||
sqflite: ^2.4.2
|
sqflite: ^2.4.2
|
||||||
|
cached_network_image: ^3.4.1
|
||||||
|
shimmer: ^3.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user