prodycrts
This commit is contained in:
233
lib/features/products/presentation/pages/products_page.dart
Normal file
233
lib/features/products/presentation/pages/products_page.dart
Normal file
@@ -0,0 +1,233 @@
|
||||
/// Page: Products Page
|
||||
///
|
||||
/// Main products browsing page with search, category filters, and product grid.
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:worker/core/constants/ui_constants.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
import 'package:worker/features/products/presentation/providers/categories_provider.dart';
|
||||
import 'package:worker/features/products/presentation/providers/products_provider.dart';
|
||||
import 'package:worker/features/products/presentation/widgets/category_filter_chips.dart';
|
||||
import 'package:worker/features/products/presentation/widgets/product_grid.dart';
|
||||
import 'package:worker/features/products/presentation/widgets/product_search_bar.dart';
|
||||
import 'package:worker/generated/l10n/app_localizations.dart';
|
||||
|
||||
/// Products Page
|
||||
///
|
||||
/// Displays the products catalog with:
|
||||
/// - Search bar
|
||||
/// - Category filter chips
|
||||
/// - Product grid
|
||||
/// - Pull-to-refresh
|
||||
/// - Loading and error states
|
||||
class ProductsPage extends ConsumerWidget {
|
||||
const ProductsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final categoriesAsync = ref.watch(categoriesProvider);
|
||||
final productsAsync = ref.watch(productsProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.white,
|
||||
appBar: AppBar(
|
||||
title: const Text('Sản phẩm', style: TextStyle(color: Colors.black)),
|
||||
elevation: AppBarSpecs.elevation,
|
||||
backgroundColor: AppColors.white,
|
||||
foregroundColor: AppColors.grey900,
|
||||
centerTitle: false,
|
||||
actions: [
|
||||
// Cart Icon with Badge
|
||||
IconButton(
|
||||
icon: const Badge(
|
||||
label: Text('3'),
|
||||
backgroundColor: AppColors.danger,
|
||||
textColor: AppColors.white,
|
||||
child: Icon(Icons.shopping_cart_outlined, color: Colors.black,),
|
||||
),
|
||||
onPressed: () {
|
||||
// TODO: Navigate to cart page
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(l10n.cart),
|
||||
duration: const Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// Search Bar
|
||||
const ProductSearchBar(),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
|
||||
// Category Filter Chips
|
||||
categoriesAsync.when(
|
||||
data: (categories) => CategoryFilterChips(categories: categories),
|
||||
loading: () => const SizedBox(
|
||||
height: 48.0,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(strokeWidth: 2.0),
|
||||
),
|
||||
),
|
||||
error: (error, stack) => const SizedBox.shrink(),
|
||||
),
|
||||
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
|
||||
// Products Grid
|
||||
Expanded(
|
||||
child: productsAsync.when(
|
||||
data: (products) {
|
||||
if (products.isEmpty) {
|
||||
return _buildEmptyState(context, l10n);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await ref.read(productsProvider.notifier).refresh();
|
||||
},
|
||||
child: ProductGrid(
|
||||
products: products,
|
||||
onProductTap: (product) {
|
||||
// TODO: Navigate to product detail page
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(product.name),
|
||||
duration: const Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
},
|
||||
onAddToCart: (product) {
|
||||
// TODO: Add to cart logic
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('${product.name} ${l10n.addedToCart}'),
|
||||
duration: const Duration(seconds: 2),
|
||||
action: SnackBarAction(
|
||||
label: l10n.viewDetails,
|
||||
onPressed: () {
|
||||
// Navigate to cart
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
loading: () => _buildLoadingState(),
|
||||
error: (error, stack) => _buildErrorState(context, l10n, error, ref),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build empty state
|
||||
Widget _buildEmptyState(BuildContext context, AppLocalizations l10n) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
size: 80.0,
|
||||
color: AppColors.grey500.withAlpha(128),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
Text(
|
||||
l10n.noProductsFound,
|
||||
style: const TextStyle(
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
Text(
|
||||
l10n.noResults,
|
||||
style: const TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build loading state
|
||||
Widget _buildLoadingState() {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: AppColors.primaryBlue,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build error state
|
||||
Widget _buildErrorState(
|
||||
BuildContext context,
|
||||
AppLocalizations l10n,
|
||||
Object error,
|
||||
WidgetRef ref,
|
||||
) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(AppSpacing.xl),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 80.0,
|
||||
color: AppColors.danger.withAlpha(128),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
Text(
|
||||
l10n.error,
|
||||
style: const TextStyle(
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
Text(
|
||||
error.toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
await ref.read(productsProvider.notifier).refresh();
|
||||
},
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: Text(l10n.tryAgain),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: AppColors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.lg,
|
||||
vertical: AppSpacing.md,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/// Provider: Categories Provider
|
||||
///
|
||||
/// Manages the state of product categories using Riverpod.
|
||||
library;
|
||||
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:worker/features/products/data/datasources/products_local_datasource.dart';
|
||||
import 'package:worker/features/products/data/repositories/products_repository_impl.dart';
|
||||
import 'package:worker/features/products/domain/entities/category.dart';
|
||||
import 'package:worker/features/products/domain/usecases/get_categories.dart';
|
||||
|
||||
part 'categories_provider.g.dart';
|
||||
|
||||
/// Categories Provider
|
||||
///
|
||||
/// Fetches and caches product categories.
|
||||
/// Automatically handles loading, error, and data states.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final categoriesAsync = ref.watch(categoriesProvider);
|
||||
///
|
||||
/// categoriesAsync.when(
|
||||
/// data: (categories) => CategoryFilterChips(categories: categories),
|
||||
/// loading: () => ShimmerLoader(),
|
||||
/// error: (error, stack) => ErrorWidget(error),
|
||||
/// );
|
||||
/// ```
|
||||
@riverpod
|
||||
Future<List<Category>> categories(Ref ref) async {
|
||||
final localDataSource = const ProductsLocalDataSourceImpl();
|
||||
final repository = ProductsRepositoryImpl(localDataSource: localDataSource);
|
||||
final useCase = GetCategories(repository);
|
||||
|
||||
return await useCase();
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'categories_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Categories Provider
|
||||
///
|
||||
/// Fetches and caches product categories.
|
||||
/// Automatically handles loading, error, and data states.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final categoriesAsync = ref.watch(categoriesProvider);
|
||||
///
|
||||
/// categoriesAsync.when(
|
||||
/// data: (categories) => CategoryFilterChips(categories: categories),
|
||||
/// loading: () => ShimmerLoader(),
|
||||
/// error: (error, stack) => ErrorWidget(error),
|
||||
/// );
|
||||
/// ```
|
||||
|
||||
@ProviderFor(categories)
|
||||
const categoriesProvider = CategoriesProvider._();
|
||||
|
||||
/// Categories Provider
|
||||
///
|
||||
/// Fetches and caches product categories.
|
||||
/// Automatically handles loading, error, and data states.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final categoriesAsync = ref.watch(categoriesProvider);
|
||||
///
|
||||
/// categoriesAsync.when(
|
||||
/// data: (categories) => CategoryFilterChips(categories: categories),
|
||||
/// loading: () => ShimmerLoader(),
|
||||
/// error: (error, stack) => ErrorWidget(error),
|
||||
/// );
|
||||
/// ```
|
||||
|
||||
final class CategoriesProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<Category>>,
|
||||
List<Category>,
|
||||
FutureOr<List<Category>>
|
||||
>
|
||||
with $FutureModifier<List<Category>>, $FutureProvider<List<Category>> {
|
||||
/// Categories Provider
|
||||
///
|
||||
/// Fetches and caches product categories.
|
||||
/// Automatically handles loading, error, and data states.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final categoriesAsync = ref.watch(categoriesProvider);
|
||||
///
|
||||
/// categoriesAsync.when(
|
||||
/// data: (categories) => CategoryFilterChips(categories: categories),
|
||||
/// loading: () => ShimmerLoader(),
|
||||
/// error: (error, stack) => ErrorWidget(error),
|
||||
/// );
|
||||
/// ```
|
||||
const CategoriesProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'categoriesProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$categoriesHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<List<Category>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<List<Category>> create(Ref ref) {
|
||||
return categories(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$categoriesHash() => r'6de35d3271d6d6572d9cdf5ed68edd26036115fc';
|
||||
@@ -0,0 +1,88 @@
|
||||
/// Provider: Products Provider
|
||||
///
|
||||
/// Manages the state of products data using Riverpod.
|
||||
/// Provides filtered products based on category and search query.
|
||||
library;
|
||||
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:worker/features/products/data/datasources/products_local_datasource.dart';
|
||||
import 'package:worker/features/products/data/repositories/products_repository_impl.dart';
|
||||
import 'package:worker/features/products/domain/entities/product.dart';
|
||||
import 'package:worker/features/products/domain/usecases/get_products.dart';
|
||||
import 'package:worker/features/products/domain/usecases/search_products.dart';
|
||||
import 'package:worker/features/products/presentation/providers/selected_category_provider.dart';
|
||||
import 'package:worker/features/products/presentation/providers/search_query_provider.dart';
|
||||
|
||||
part 'products_provider.g.dart';
|
||||
|
||||
/// Products Provider
|
||||
///
|
||||
/// Fetches and filters products based on selected category and search query.
|
||||
/// Automatically updates when category or search query changes.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final productsAsync = ref.watch(productsProvider);
|
||||
///
|
||||
/// productsAsync.when(
|
||||
/// data: (products) => ProductGrid(products: products),
|
||||
/// loading: () => CircularProgressIndicator(),
|
||||
/// error: (error, stack) => ErrorWidget(error),
|
||||
/// );
|
||||
/// ```
|
||||
@riverpod
|
||||
class Products extends _$Products {
|
||||
@override
|
||||
Future<List<Product>> build() async {
|
||||
// Watch dependencies
|
||||
final selectedCategory = ref.watch(selectedCategoryProvider);
|
||||
final searchQuery = ref.watch(searchQueryProvider);
|
||||
|
||||
// Initialize dependencies
|
||||
final localDataSource = const ProductsLocalDataSourceImpl();
|
||||
final repository = ProductsRepositoryImpl(localDataSource: localDataSource);
|
||||
|
||||
// Apply filters
|
||||
List<Product> products;
|
||||
|
||||
if (searchQuery.isNotEmpty) {
|
||||
// Search takes precedence over category filter
|
||||
final searchUseCase = SearchProducts(repository);
|
||||
products = await searchUseCase(searchQuery);
|
||||
|
||||
// If a category is selected, filter search results by category
|
||||
if (selectedCategory != 'all') {
|
||||
products = products
|
||||
.where((product) => product.categoryId == selectedCategory)
|
||||
.toList();
|
||||
}
|
||||
} else {
|
||||
// No search query, use category filter
|
||||
final getProductsUseCase = GetProducts(repository);
|
||||
products = await getProductsUseCase(categoryId: selectedCategory);
|
||||
}
|
||||
|
||||
return products;
|
||||
}
|
||||
|
||||
/// Refresh products data
|
||||
///
|
||||
/// Forces a refresh from the datasource.
|
||||
Future<void> refresh() async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() => build());
|
||||
}
|
||||
}
|
||||
|
||||
/// All Products Provider (no filters)
|
||||
///
|
||||
/// Provides all products without any filtering.
|
||||
/// Useful for product selection dialogs, etc.
|
||||
@riverpod
|
||||
Future<List<Product>> allProducts(Ref ref) async {
|
||||
final localDataSource = const ProductsLocalDataSourceImpl();
|
||||
final repository = ProductsRepositoryImpl(localDataSource: localDataSource);
|
||||
final getProductsUseCase = GetProducts(repository);
|
||||
|
||||
return await getProductsUseCase();
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'products_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Products Provider
|
||||
///
|
||||
/// Fetches and filters products based on selected category and search query.
|
||||
/// Automatically updates when category or search query changes.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final productsAsync = ref.watch(productsProvider);
|
||||
///
|
||||
/// productsAsync.when(
|
||||
/// data: (products) => ProductGrid(products: products),
|
||||
/// loading: () => CircularProgressIndicator(),
|
||||
/// error: (error, stack) => ErrorWidget(error),
|
||||
/// );
|
||||
/// ```
|
||||
|
||||
@ProviderFor(Products)
|
||||
const productsProvider = ProductsProvider._();
|
||||
|
||||
/// Products Provider
|
||||
///
|
||||
/// Fetches and filters products based on selected category and search query.
|
||||
/// Automatically updates when category or search query changes.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final productsAsync = ref.watch(productsProvider);
|
||||
///
|
||||
/// productsAsync.when(
|
||||
/// data: (products) => ProductGrid(products: products),
|
||||
/// loading: () => CircularProgressIndicator(),
|
||||
/// error: (error, stack) => ErrorWidget(error),
|
||||
/// );
|
||||
/// ```
|
||||
final class ProductsProvider
|
||||
extends $AsyncNotifierProvider<Products, List<Product>> {
|
||||
/// Products Provider
|
||||
///
|
||||
/// Fetches and filters products based on selected category and search query.
|
||||
/// Automatically updates when category or search query changes.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final productsAsync = ref.watch(productsProvider);
|
||||
///
|
||||
/// productsAsync.when(
|
||||
/// data: (products) => ProductGrid(products: products),
|
||||
/// loading: () => CircularProgressIndicator(),
|
||||
/// error: (error, stack) => ErrorWidget(error),
|
||||
/// );
|
||||
/// ```
|
||||
const ProductsProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'productsProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$productsHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
Products create() => Products();
|
||||
}
|
||||
|
||||
String _$productsHash() => r'0f1b32d2c14b9d8d600ffb0270f54d32af753e1f';
|
||||
|
||||
/// Products Provider
|
||||
///
|
||||
/// Fetches and filters products based on selected category and search query.
|
||||
/// Automatically updates when category or search query changes.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final productsAsync = ref.watch(productsProvider);
|
||||
///
|
||||
/// productsAsync.when(
|
||||
/// data: (products) => ProductGrid(products: products),
|
||||
/// loading: () => CircularProgressIndicator(),
|
||||
/// error: (error, stack) => ErrorWidget(error),
|
||||
/// );
|
||||
/// ```
|
||||
|
||||
abstract class _$Products extends $AsyncNotifier<List<Product>> {
|
||||
FutureOr<List<Product>> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<AsyncValue<List<Product>>, List<Product>>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<AsyncValue<List<Product>>, List<Product>>,
|
||||
AsyncValue<List<Product>>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
|
||||
/// All Products Provider (no filters)
|
||||
///
|
||||
/// Provides all products without any filtering.
|
||||
/// Useful for product selection dialogs, etc.
|
||||
|
||||
@ProviderFor(allProducts)
|
||||
const allProductsProvider = AllProductsProvider._();
|
||||
|
||||
/// All Products Provider (no filters)
|
||||
///
|
||||
/// Provides all products without any filtering.
|
||||
/// Useful for product selection dialogs, etc.
|
||||
|
||||
final class AllProductsProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<Product>>,
|
||||
List<Product>,
|
||||
FutureOr<List<Product>>
|
||||
>
|
||||
with $FutureModifier<List<Product>>, $FutureProvider<List<Product>> {
|
||||
/// All Products Provider (no filters)
|
||||
///
|
||||
/// Provides all products without any filtering.
|
||||
/// Useful for product selection dialogs, etc.
|
||||
const AllProductsProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'allProductsProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$allProductsHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<List<Product>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<List<Product>> create(Ref ref) {
|
||||
return allProducts(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$allProductsHash() => r'a02e989ad36e644d9b62e681b3ced88e10e4d4c3';
|
||||
@@ -0,0 +1,39 @@
|
||||
/// Provider: Search Query Provider
|
||||
///
|
||||
/// Manages the current search query state for product filtering.
|
||||
library;
|
||||
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'search_query_provider.g.dart';
|
||||
|
||||
/// Search Query Provider
|
||||
///
|
||||
/// Holds the current search query string for filtering products.
|
||||
/// Default is empty string which shows all products.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// // Read the current value
|
||||
/// final searchQuery = ref.watch(searchQueryProvider);
|
||||
///
|
||||
/// // Update the value
|
||||
/// ref.read(searchQueryProvider.notifier).state = 'gạch men';
|
||||
/// ```
|
||||
@riverpod
|
||||
class SearchQuery extends _$SearchQuery {
|
||||
@override
|
||||
String build() {
|
||||
return ''; // Default: no search filter
|
||||
}
|
||||
|
||||
/// Update search query
|
||||
void updateQuery(String query) {
|
||||
state = query;
|
||||
}
|
||||
|
||||
/// Clear search query
|
||||
void clear() {
|
||||
state = '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'search_query_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Search Query Provider
|
||||
///
|
||||
/// Holds the current search query string for filtering products.
|
||||
/// Default is empty string which shows all products.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// // Read the current value
|
||||
/// final searchQuery = ref.watch(searchQueryProvider);
|
||||
///
|
||||
/// // Update the value
|
||||
/// ref.read(searchQueryProvider.notifier).state = 'gạch men';
|
||||
/// ```
|
||||
|
||||
@ProviderFor(SearchQuery)
|
||||
const searchQueryProvider = SearchQueryProvider._();
|
||||
|
||||
/// Search Query Provider
|
||||
///
|
||||
/// Holds the current search query string for filtering products.
|
||||
/// Default is empty string which shows all products.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// // Read the current value
|
||||
/// final searchQuery = ref.watch(searchQueryProvider);
|
||||
///
|
||||
/// // Update the value
|
||||
/// ref.read(searchQueryProvider.notifier).state = 'gạch men';
|
||||
/// ```
|
||||
final class SearchQueryProvider extends $NotifierProvider<SearchQuery, String> {
|
||||
/// Search Query Provider
|
||||
///
|
||||
/// Holds the current search query string for filtering products.
|
||||
/// Default is empty string which shows all products.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// // Read the current value
|
||||
/// final searchQuery = ref.watch(searchQueryProvider);
|
||||
///
|
||||
/// // Update the value
|
||||
/// ref.read(searchQueryProvider.notifier).state = 'gạch men';
|
||||
/// ```
|
||||
const SearchQueryProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'searchQueryProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$searchQueryHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
SearchQuery create() => SearchQuery();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(String value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<String>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$searchQueryHash() => r'41ea2fa57593abc0cafe16598d8817584ba99ddc';
|
||||
|
||||
/// Search Query Provider
|
||||
///
|
||||
/// Holds the current search query string for filtering products.
|
||||
/// Default is empty string which shows all products.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// // Read the current value
|
||||
/// final searchQuery = ref.watch(searchQueryProvider);
|
||||
///
|
||||
/// // Update the value
|
||||
/// ref.read(searchQueryProvider.notifier).state = 'gạch men';
|
||||
/// ```
|
||||
|
||||
abstract class _$SearchQuery extends $Notifier<String> {
|
||||
String build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<String, String>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<String, String>,
|
||||
String,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/// Provider: Selected Category Provider
|
||||
///
|
||||
/// Manages the currently selected category filter state.
|
||||
library;
|
||||
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'selected_category_provider.g.dart';
|
||||
|
||||
/// Selected Category Provider
|
||||
///
|
||||
/// Holds the currently selected category ID for filtering products.
|
||||
/// Default is 'all' which shows all products.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// // Read the current value
|
||||
/// final selectedCategory = ref.watch(selectedCategoryProvider);
|
||||
///
|
||||
/// // Update the value
|
||||
/// ref.read(selectedCategoryProvider.notifier).state = 'floor_tiles';
|
||||
/// ```
|
||||
@riverpod
|
||||
class SelectedCategory extends _$SelectedCategory {
|
||||
@override
|
||||
String build() {
|
||||
return 'all'; // Default: show all products
|
||||
}
|
||||
|
||||
/// Update selected category
|
||||
void updateCategory(String categoryId) {
|
||||
state = categoryId;
|
||||
}
|
||||
|
||||
/// Reset to show all products
|
||||
void reset() {
|
||||
state = 'all';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'selected_category_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Selected Category Provider
|
||||
///
|
||||
/// Holds the currently selected category ID for filtering products.
|
||||
/// Default is 'all' which shows all products.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// // Read the current value
|
||||
/// final selectedCategory = ref.watch(selectedCategoryProvider);
|
||||
///
|
||||
/// // Update the value
|
||||
/// ref.read(selectedCategoryProvider.notifier).state = 'floor_tiles';
|
||||
/// ```
|
||||
|
||||
@ProviderFor(SelectedCategory)
|
||||
const selectedCategoryProvider = SelectedCategoryProvider._();
|
||||
|
||||
/// Selected Category Provider
|
||||
///
|
||||
/// Holds the currently selected category ID for filtering products.
|
||||
/// Default is 'all' which shows all products.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// // Read the current value
|
||||
/// final selectedCategory = ref.watch(selectedCategoryProvider);
|
||||
///
|
||||
/// // Update the value
|
||||
/// ref.read(selectedCategoryProvider.notifier).state = 'floor_tiles';
|
||||
/// ```
|
||||
final class SelectedCategoryProvider
|
||||
extends $NotifierProvider<SelectedCategory, String> {
|
||||
/// Selected Category Provider
|
||||
///
|
||||
/// Holds the currently selected category ID for filtering products.
|
||||
/// Default is 'all' which shows all products.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// // Read the current value
|
||||
/// final selectedCategory = ref.watch(selectedCategoryProvider);
|
||||
///
|
||||
/// // Update the value
|
||||
/// ref.read(selectedCategoryProvider.notifier).state = 'floor_tiles';
|
||||
/// ```
|
||||
const SelectedCategoryProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'selectedCategoryProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$selectedCategoryHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
SelectedCategory create() => SelectedCategory();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(String value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<String>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$selectedCategoryHash() => r'269171acff2e04353101596c8d65f46fa54dc839';
|
||||
|
||||
/// Selected Category Provider
|
||||
///
|
||||
/// Holds the currently selected category ID for filtering products.
|
||||
/// Default is 'all' which shows all products.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// // Read the current value
|
||||
/// final selectedCategory = ref.watch(selectedCategoryProvider);
|
||||
///
|
||||
/// // Update the value
|
||||
/// ref.read(selectedCategoryProvider.notifier).state = 'floor_tiles';
|
||||
/// ```
|
||||
|
||||
abstract class _$SelectedCategory extends $Notifier<String> {
|
||||
String build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<String, String>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<String, String>,
|
||||
String,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/// Widget: Category Filter Chips
|
||||
///
|
||||
/// Horizontal scrolling filter chips for product categories.
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:worker/core/constants/ui_constants.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
import 'package:worker/features/products/domain/entities/category.dart';
|
||||
import 'package:worker/features/products/presentation/providers/selected_category_provider.dart';
|
||||
|
||||
/// Category Filter Chips Widget
|
||||
///
|
||||
/// Displays categories as horizontally scrolling chips.
|
||||
/// Updates selected category when tapped.
|
||||
class CategoryFilterChips extends ConsumerWidget {
|
||||
final List<Category> categories;
|
||||
|
||||
const CategoryFilterChips({
|
||||
super.key,
|
||||
required this.categories,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final selectedCategory = ref.watch(selectedCategoryProvider);
|
||||
|
||||
return SizedBox(
|
||||
height: 48.0,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||
itemCount: categories.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(width: AppSpacing.sm),
|
||||
itemBuilder: (context, index) {
|
||||
final category = categories[index];
|
||||
final isSelected = selectedCategory == category.id;
|
||||
|
||||
return FilterChip(
|
||||
label: Text(
|
||||
category.name,
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||
color: isSelected ? AppColors.white : AppColors.grey900,
|
||||
),
|
||||
),
|
||||
selected: isSelected,
|
||||
onSelected: (selected) {
|
||||
if (selected) {
|
||||
ref.read(selectedCategoryProvider.notifier).updateCategory(category.id);
|
||||
}
|
||||
},
|
||||
backgroundColor: AppColors.white,
|
||||
selectedColor: AppColors.primaryBlue,
|
||||
checkmarkColor: AppColors.white,
|
||||
side: BorderSide(
|
||||
color: isSelected ? AppColors.primaryBlue : AppColors.grey100,
|
||||
width: isSelected ? 2.0 : 1.0,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.button),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md,
|
||||
vertical: AppSpacing.sm,
|
||||
),
|
||||
elevation: isSelected ? AppElevation.low : 0,
|
||||
showCheckmark: false,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
208
lib/features/products/presentation/widgets/product_card.dart
Normal file
208
lib/features/products/presentation/widgets/product_card.dart
Normal file
@@ -0,0 +1,208 @@
|
||||
/// Widget: Product Card
|
||||
///
|
||||
/// Displays a product in a card format with image, name, price, and add to cart button.
|
||||
library;
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
import 'package:worker/core/constants/ui_constants.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
import 'package:worker/features/products/domain/entities/product.dart';
|
||||
import 'package:worker/generated/l10n/app_localizations.dart';
|
||||
|
||||
/// Product Card Widget
|
||||
///
|
||||
/// Displays product information in a card format.
|
||||
/// Includes image, name, price, stock status, and add to cart button.
|
||||
class ProductCard extends StatelessWidget {
|
||||
final Product product;
|
||||
final VoidCallback? onTap;
|
||||
final VoidCallback? onAddToCart;
|
||||
|
||||
const ProductCard({
|
||||
super.key,
|
||||
required this.product,
|
||||
this.onTap,
|
||||
this.onAddToCart,
|
||||
});
|
||||
|
||||
String _formatPrice(double price) {
|
||||
final formatter = NumberFormat('#,###', 'vi_VN');
|
||||
return '${formatter.format(price)}đ';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
return Card(
|
||||
elevation: ProductCardSpecs.elevation,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(ProductCardSpecs.borderRadius),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(ProductCardSpecs.borderRadius),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Product Image
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
// Image
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(ProductCardSpecs.borderRadius),
|
||||
),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: product.imageUrl,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
memCacheWidth: ImageSpecs.productImageCacheWidth,
|
||||
memCacheHeight: ImageSpecs.productImageCacheHeight,
|
||||
placeholder: (context, url) => Shimmer.fromColors(
|
||||
baseColor: AppColors.grey100,
|
||||
highlightColor: AppColors.grey50,
|
||||
child: Container(
|
||||
color: AppColors.grey100,
|
||||
),
|
||||
),
|
||||
errorWidget: (context, url, error) => Container(
|
||||
color: AppColors.grey100,
|
||||
child: const Icon(
|
||||
Icons.image_not_supported,
|
||||
size: 48.0,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Sale Badge
|
||||
if (product.isOnSale)
|
||||
Positioned(
|
||||
top: AppSpacing.sm,
|
||||
right: AppSpacing.sm,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.sm,
|
||||
vertical: 4.0,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.danger,
|
||||
borderRadius: BorderRadius.circular(AppRadius.sm),
|
||||
),
|
||||
child: Text(
|
||||
'-${product.discountPercentage}%',
|
||||
style: const TextStyle(
|
||||
color: AppColors.white,
|
||||
fontSize: 11.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Low Stock Badge
|
||||
if (product.isLowStock)
|
||||
Positioned(
|
||||
top: AppSpacing.sm,
|
||||
left: AppSpacing.sm,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.sm,
|
||||
vertical: 4.0,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.warning,
|
||||
borderRadius: BorderRadius.circular(AppRadius.sm),
|
||||
),
|
||||
child: Text(
|
||||
l10n.lowStock,
|
||||
style: const TextStyle(
|
||||
color: AppColors.white,
|
||||
fontSize: 11.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Product Info
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(AppSpacing.sm),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Product Name
|
||||
Text(
|
||||
product.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 14.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.3,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
|
||||
// Price
|
||||
Text(
|
||||
'${_formatPrice(product.effectivePrice)}/${product.unit}',
|
||||
style: const TextStyle(
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.primaryBlue,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
|
||||
// Add to Cart Button - Full Width
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 36.0,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: product.inStock ? onAddToCart : null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: AppColors.white,
|
||||
disabledBackgroundColor: AppColors.grey100,
|
||||
disabledForegroundColor: AppColors.grey500,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.button),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.sm,
|
||||
),
|
||||
),
|
||||
icon: const Icon(Icons.shopping_cart, size: 18.0),
|
||||
label: Text(
|
||||
product.inStock ? l10n.addToCart : l10n.outOfStock,
|
||||
style: const TextStyle(
|
||||
fontSize: 14.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
48
lib/features/products/presentation/widgets/product_grid.dart
Normal file
48
lib/features/products/presentation/widgets/product_grid.dart
Normal file
@@ -0,0 +1,48 @@
|
||||
/// Widget: Product Grid
|
||||
///
|
||||
/// Grid view displaying product cards.
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:worker/core/constants/ui_constants.dart';
|
||||
import 'package:worker/features/products/domain/entities/product.dart';
|
||||
import 'package:worker/features/products/presentation/widgets/product_card.dart';
|
||||
|
||||
/// Product Grid Widget
|
||||
///
|
||||
/// Displays products in a 2-column grid layout.
|
||||
class ProductGrid extends StatelessWidget {
|
||||
final List<Product> products;
|
||||
final void Function(Product)? onProductTap;
|
||||
final void Function(Product)? onAddToCart;
|
||||
|
||||
const ProductGrid({
|
||||
super.key,
|
||||
required this.products,
|
||||
this.onProductTap,
|
||||
this.onAddToCart,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GridView.builder(
|
||||
padding: const EdgeInsets.all(AppSpacing.xs),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: GridSpecs.productGridColumns,
|
||||
crossAxisSpacing: AppSpacing.xs,
|
||||
mainAxisSpacing: AppSpacing.xs,
|
||||
childAspectRatio: 0.7, // Width / Height ratio
|
||||
),
|
||||
itemCount: products.length,
|
||||
itemBuilder: (context, index) {
|
||||
final product = products[index];
|
||||
|
||||
return ProductCard(
|
||||
product: product,
|
||||
onTap: onProductTap != null ? () => onProductTap!(product) : null,
|
||||
onAddToCart: onAddToCart != null ? () => onAddToCart!(product) : null,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/// Widget: Product Search Bar
|
||||
///
|
||||
/// Custom search bar for filtering products.
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:worker/core/constants/ui_constants.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
import 'package:worker/features/products/presentation/providers/search_query_provider.dart';
|
||||
import 'package:worker/generated/l10n/app_localizations.dart';
|
||||
|
||||
/// Product Search Bar Widget
|
||||
///
|
||||
/// A search input field that updates the search query provider.
|
||||
/// Includes search icon and clear button.
|
||||
class ProductSearchBar extends ConsumerStatefulWidget {
|
||||
const ProductSearchBar({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ProductSearchBar> createState() => _ProductSearchBarState();
|
||||
}
|
||||
|
||||
class _ProductSearchBarState extends ConsumerState<ProductSearchBar> {
|
||||
late final TextEditingController _controller;
|
||||
late final FocusNode _focusNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController();
|
||||
_focusNode = FocusNode();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onSearchChanged(String value) {
|
||||
// Update search query provider
|
||||
ref.read(searchQueryProvider.notifier).updateQuery(value);
|
||||
}
|
||||
|
||||
void _onClearSearch() {
|
||||
_controller.clear();
|
||||
ref.read(searchQueryProvider.notifier).clear();
|
||||
_focusNode.unfocus();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
return Container(
|
||||
height: InputFieldSpecs.height,
|
||||
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
onChanged: _onSearchChanged,
|
||||
decoration: InputDecoration(
|
||||
hintText: l10n.searchProducts,
|
||||
hintStyle: const TextStyle(
|
||||
fontSize: InputFieldSpecs.hintFontSize,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
prefixIcon: const Icon(
|
||||
Icons.search,
|
||||
color: AppColors.grey500,
|
||||
size: AppIconSize.md,
|
||||
),
|
||||
suffixIcon: _controller.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(
|
||||
Icons.clear,
|
||||
color: AppColors.grey500,
|
||||
size: AppIconSize.md,
|
||||
),
|
||||
onPressed: _onClearSearch,
|
||||
)
|
||||
: null,
|
||||
filled: true,
|
||||
fillColor: const Color(0xFFF5F5F5),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||
borderSide: const BorderSide(
|
||||
color: AppColors.primaryBlue,
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md,
|
||||
vertical: AppSpacing.md,
|
||||
),
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: InputFieldSpecs.fontSize,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user