QueenFruits/Mobile Commerce/lib/features/home/presentation/screens/search_screen.dart

549 lines
20 KiB
Dart

import 'dart:async';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:niogu_ecommerce_v1/core/constant/app_font_size.dart';
import 'package:niogu_ecommerce_v1/core/errors/exceptions.dart';
import 'package:niogu_ecommerce_v1/core/providers/app_provider.dart';
import 'package:niogu_ecommerce_v1/core/router/app_route.dart';
import 'package:niogu_ecommerce_v1/core/utils/currency_format.dart';
import 'package:niogu_ecommerce_v1/core/utils/extenstion_format.dart';
import 'package:niogu_ecommerce_v1/core/utils/log_message.dart';
import 'package:niogu_ecommerce_v1/core/widgets/custom_empty_screen.dart';
import 'package:niogu_ecommerce_v1/core/widgets/custom_snackbar.dart';
import 'package:niogu_ecommerce_v1/features/favorite/domain/entities/favorite.dart';
import 'package:niogu_ecommerce_v1/features/favorite/presentation/providers/favorite_provider.dart';
import 'package:niogu_ecommerce_v1/features/home/domain/entities/home.dart';
import 'package:niogu_ecommerce_v1/features/home/presentation/providers/home_provider.dart';
import 'package:niogu_ecommerce_v1/features/product/presentation/providers/product_provider.dart';
import 'package:sizer/sizer.dart';
import 'package:niogu_ecommerce_v1/core/constant/app_color.dart';
class SearchScreen extends ConsumerStatefulWidget {
final List<CategoryItem> categories;
const SearchScreen({super.key, required this.categories});
@override
ConsumerState<SearchScreen> createState() => _SearchScreenState();
}
class _SearchScreenState extends ConsumerState<SearchScreen> {
late final List<CategoryItem> _categories;
final TextEditingController _searchController = TextEditingController();
Timer? _debounce;
@override
void initState() {
// TODO: implement initState
super.initState();
_categories = widget.categories;
}
@override
void dispose() {
_searchController.dispose();
_debounce?.cancel();
super.dispose();
}
void _onSearchChanged(String query) {
if (_debounce?.isActive ?? false) _debounce?.cancel();
_debounce = Timer(const Duration(milliseconds: 800), () {
final processSerch = ref.read(processSearchStateProvider);
if (processSerch != null) {
ref.read(processSearchStateProvider.notifier).state = null;
}
ref.read(productSearchProvider.notifier).state = query;
});
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final productByNameState = ref.watch(productByNameProvider);
final processSearchState = ref.watch(processSearchProvider);
final currentOutletId = ref.watch(currentOutletIdProvider);
final favoriteState = ref.watch(favoriteControllerProvider);
return SafeArea(
top: false,
bottom: true,
right: false,
left: false,
child: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0.5,
leading: IconButton(
icon: Icon(
Icons.arrow_back,
size: 7.w,
color: AppColor.primaryColor,
),
onPressed: () => context.pop(),
),
title: Container(
height: 5.h,
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(2.w),
),
child: TextField(
controller: _searchController,
autofocus: true,
onChanged: _onSearchChanged,
style: TextStyle(fontSize: AppFontSize.small.sp),
decoration: InputDecoration(
hintText: "Ketik nama produk...",
hintStyle: TextStyle(fontSize: AppFontSize.small.sp),
prefixIcon: Icon(Icons.search, size: 5.w),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
icon: Icon(
Icons.cancel,
color: Colors.grey,
size: 5.w,
),
onPressed: () {
_searchController.clear();
_onSearchChanged("");
},
)
: null,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 1.h),
),
),
),
),
body: productByNameState.when(
data: (products) {
switch (processSearchState) {
case ProcessSearch.initial:
return _buildInitialRecommended();
case ProcessSearch.suggestion:
return _buildSuggestionList(products);
case ProcessSearch.result:
return _buildProductResult(
products: products,
currentOutletId: currentOutletId!,
favorites: favoriteState,
);
}
},
error: (error, stackTrace) {
return SizedBox(
height: 80.h,
child: CustomEmptyScreen(
icon: Icons.cloud_off_outlined,
title: "Terjadi Kesalahan Koneksi",
subtitle: "Tarik ke bawah untuk mencoba lagi",
height: 80.h,
),
);
},
loading: () => const Center(
child: CircularProgressIndicator(color: AppColor.primaryColor),
),
),
),
);
},
);
}
Widget _buildInitialRecommended() {
return Padding(
padding: EdgeInsets.all(4.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Rekomendasi",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: AppFontSize.small.sp,
),
),
SizedBox(height: 2.h),
GridView.builder(
shrinkWrap: true,
itemCount: _categories.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
mainAxisSpacing: 2.h,
childAspectRatio: 0.8,
),
itemBuilder: (context, index) {
final category = _categories[index];
return GestureDetector(
onTap: () {
context.pushNamed(
AppRoute.productCategoryScreen,
extra: category,
);
},
child: Padding(
padding: EdgeInsets.only(right: 5.w),
child: Column(
children: [
CachedNetworkImage(
imageUrl: category.image ?? 'error',
imageBuilder: (context, imageProvider) {
return Container(
height: 18.w,
width: 18.w,
decoration: BoxDecoration(
color: Colors.white,
border: BoxBorder.all(
color: Colors.grey.shade300,
),
borderRadius: BorderRadius.circular(2.5.w),
image: DecorationImage(
image: imageProvider,
fit: BoxFit.cover,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
),
],
),
);
},
errorWidget: (context, url, error) {
return Container(
height: 18.w,
width: 18.w,
decoration: BoxDecoration(
color: Colors.white,
border: BoxBorder.all(
color: Colors.grey.shade300,
),
borderRadius: BorderRadius.circular(2.5.w),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
),
],
),
child: Icon(
Icons.image_outlined,
size: 5.w,
color: Colors.grey.shade300,
),
);
},
),
SizedBox(height: 1.h),
Text(
category.name,
style: TextStyle(
fontSize: (AppFontSize.small - 2).sp,
color: Color(0xFF102851),
),
),
],
),
),
);
},
),
],
),
);
}
Widget _buildSuggestionList(List<ProductItem> products) {
if (products.isEmpty) {
return CustomEmptyScreen(
icon: Icons.search_off_outlined,
title: "Produk tidak tersedia",
height: 40.h,
);
}
return ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return ListTile(
leading: Icon(Icons.search, color: Colors.grey, size: 5.w),
title: Text(
product.name,
style: TextStyle(
color: Colors.grey.shade700,
fontSize: AppFontSize.small.sp,
),
),
trailing: Icon(Icons.north_west, color: Colors.grey, size: 5.w),
onTap: () {
ref.read(processSearchStateProvider.notifier).state =
ProcessSearch.result;
_searchController.text = product.name;
},
);
},
);
}
Widget _buildProductResult({
required List<ProductItem> products,
required String currentOutletId,
required Map<String, SelectedFavorite> favorites,
}) {
return GridView.builder(
padding: EdgeInsets.all(4.w),
itemCount: products.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.7,
crossAxisSpacing: 3.w,
mainAxisSpacing: 3.w,
),
itemBuilder: (context, index) {
final product = products[index];
final isFavorite = favorites.containsKey(
"$currentOutletId-${product.id}",
);
return _buildProductCard(product, currentOutletId, isFavorite);
},
);
}
Widget _buildProductCard(
ProductItem product,
String currentOutletId,
bool isFavorite,
) {
return GestureDetector(
onTap: () async {
try {
final productDetail = await ref
.read(productRepositoryProvider)
.fetchProductById(product.id);
if (productDetail == null) {
CustomSnackbar.showError(context, "Produk tidak ditemukan");
ref.read(homeControllerProvider.notifier).refresh();
return;
}
context.pushNamed(AppRoute.productDetailScreen, extra: productDetail);
} on ServerException catch (e, st) {
LogMessage.log.i(e.toString(), error: e, stackTrace: st);
}
},
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(2.5.w),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 3,
child: CachedNetworkImage(
imageUrl: product.image ?? 'error',
imageBuilder: (context, imageProvider) {
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.vertical(
top: Radius.circular(2.5.w),
),
image: DecorationImage(
image: imageProvider,
fit: BoxFit.cover,
),
),
child: Stack(
children: [
Positioned(
top: 1.w,
right: 1.w,
child: GestureDetector(
onTap: () {
final currentOutletId = ref.read(
currentOutletIdProvider,
);
ref
.read(favoriteControllerProvider.notifier)
.toggle(product, currentOutletId!);
},
child: CircleAvatar(
radius: 14,
backgroundColor: Colors.white.withOpacity(0.9),
child: Icon(
isFavorite
? Icons.favorite
: Icons.favorite_outline,
size: 5.w,
color: isFavorite ? Colors.red : Colors.grey,
),
),
),
),
],
),
);
},
errorWidget: (context, url, error) {
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.vertical(
top: Radius.circular(2.5.w),
),
),
child: Stack(
children: [
Center(
child: Icon(
Icons.image_outlined,
color: Colors.grey.shade300,
size: 10.w,
),
),
Positioned(
top: 1.w,
right: 1.w,
child: GestureDetector(
onTap: () {
ref
.read(favoriteControllerProvider.notifier)
.toggle(product, currentOutletId);
},
child: CircleAvatar(
radius: 14,
backgroundColor: Colors.white.withOpacity(0.9),
child: Icon(
isFavorite
? Icons.favorite
: Icons.favorite_outline,
size: 5.w,
color: isFavorite ? Colors.red : Colors.grey,
),
),
),
),
],
),
);
},
),
),
Expanded(
flex: 2,
child: Padding(
padding: EdgeInsets.all(3.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
product.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: (AppFontSize.small - 1.25).sp,
color: const Color(0xFF102851),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"${product.totalSold.toCompact} Terjual",
style: TextStyle(
color: Colors.black87,
fontWeight: FontWeight.bold,
fontSize: (AppFontSize.small - 1.25).sp,
),
),
Row(
children: [
Icon(Icons.star, color: Colors.orange, size: 3.5.w),
SizedBox(width: 1.w),
Text(
product.averageRating.toStringAsFixed(1),
style: TextStyle(
fontSize: (AppFontSize.small - 1.25).sp,
color: Colors.grey.shade600,
),
),
],
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
CurrencyFormat.formatToIdr(product.sellingPrice, 0),
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: AppFontSize.small.sp,
color: AppColor.primaryColor,
),
),
Row(
children: [
Icon(
Icons.thumb_up,
color: Colors.red,
size: 3.5.w,
),
SizedBox(width: 1.w),
Text(
product.likes.toString(),
style: TextStyle(
fontSize: (AppFontSize.small - 1.25).sp,
color: Colors.red,
),
),
],
),
],
),
],
),
),
),
],
),
),
);
}
}