product and category loader
This commit is contained in:
@@ -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]),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user