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:dartz/dartz.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
import '../../../domain/category/category.dart';
|
||||
|
||||
@ -11,6 +12,7 @@ part 'category_loader_event.dart';
|
||||
part 'category_loader_state.dart';
|
||||
part 'category_loader_bloc.freezed.dart';
|
||||
|
||||
@injectable
|
||||
class CategoryLoaderBloc
|
||||
extends Bloc<CategoryLoaderEvent, CategoryLoaderState> {
|
||||
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:intl/intl.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/login_form/login_form_bloc.dart'
|
||||
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'
|
||||
as _i76;
|
||||
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>(
|
||||
() => _i46.LoginFormBloc(gh<_i776.IAuthRepository>()),
|
||||
);
|
||||
gh.factory<_i1018.CategoryLoaderBloc>(
|
||||
() => _i1018.CategoryLoaderBloc(gh<_i502.ICategoryRepository>()),
|
||||
);
|
||||
gh.factory<_i343.AuthBloc>(
|
||||
() => _i343.AuthBloc(
|
||||
gh<_i776.IAuthRepository>(),
|
||||
|
||||
@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../application/auth/auth_bloc.dart';
|
||||
import '../application/category/category_loader/category_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/constant/app_constant.dart';
|
||||
import '../injection.dart';
|
||||
@ -27,6 +28,7 @@ class _AppWidgetState extends State<AppWidget> {
|
||||
BlocProvider(create: (context) => getIt<AuthBloc>()),
|
||||
BlocProvider(create: (context) => getIt<OutletLoaderBloc>()),
|
||||
BlocProvider(create: (context) => getIt<CategoryLoaderBloc>()),
|
||||
BlocProvider(create: (context) => getIt<ProductLoaderBloc>()),
|
||||
],
|
||||
child: MaterialApp.router(
|
||||
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: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 '../../../../../injection.dart';
|
||||
import 'widgets/home_left_panel.dart';
|
||||
import 'widgets/home_right_panel.dart';
|
||||
|
||||
@RoutePage()
|
||||
class HomePage extends StatelessWidget {
|
||||
class HomePage extends StatelessWidget implements AutoRouteWrapper {
|
||||
const HomePage({super.key});
|
||||
|
||||
@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';
|
||||
|
||||
class HomeLeftPanel extends StatelessWidget {
|
||||
class HomeLeftPanel extends StatefulWidget {
|
||||
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
|
||||
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(
|
||||
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(
|
||||
name,
|
||||
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(
|
||||
name,
|
||||
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"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -406,6 +430,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -704,6 +736,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -848,6 +888,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
rxdart:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: rxdart
|
||||
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.28.0"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -920,6 +968,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@ -949,6 +1005,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.1"
|
||||
sprintf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sprintf
|
||||
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
sqflite:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1069,6 +1133,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: uuid
|
||||
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.1"
|
||||
vector_graphics:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@ -34,6 +34,8 @@ dependencies:
|
||||
bloc: ^9.1.0
|
||||
flutter_bloc: ^9.1.1
|
||||
sqflite: ^2.4.2
|
||||
cached_network_image: ^3.4.1
|
||||
shimmer: ^3.0.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user