diff --git a/lib/features/categories/data/datasources/category_remote_datasource.dart b/lib/features/categories/data/datasources/category_remote_datasource.dart new file mode 100644 index 0000000..434c0d8 --- /dev/null +++ b/lib/features/categories/data/datasources/category_remote_datasource.dart @@ -0,0 +1,51 @@ +import '../models/category_model.dart'; +import '../../../../core/network/dio_client.dart'; +import '../../../../core/constants/api_constants.dart'; +import '../../../../core/errors/exceptions.dart'; + +/// Category remote data source using API +abstract class CategoryRemoteDataSource { + Future> getAllCategories(); + Future getCategoryById(String id); +} + +class CategoryRemoteDataSourceImpl implements CategoryRemoteDataSource { + final DioClient client; + + CategoryRemoteDataSourceImpl(this.client); + + @override + Future> getAllCategories() async { + try { + final response = await client.get(ApiConstants.categories); + + // API returns: { success: true, data: [...categories...] } + if (response.data['success'] == true) { + final List data = response.data['data'] ?? []; + return data.map((json) => CategoryModel.fromJson(json)).toList(); + } else { + throw ServerException(response.data['message'] ?? 'Failed to fetch categories'); + } + } catch (e) { + if (e is ServerException) rethrow; + throw ServerException('Failed to fetch categories: $e'); + } + } + + @override + Future getCategoryById(String id) async { + try { + final response = await client.get(ApiConstants.categoryById(id)); + + // API returns: { success: true, data: {...category...} } + if (response.data['success'] == true) { + return CategoryModel.fromJson(response.data['data']); + } else { + throw ServerException(response.data['message'] ?? 'Category not found'); + } + } catch (e) { + if (e is ServerException) rethrow; + throw ServerException('Failed to fetch category: $e'); + } + } +} diff --git a/lib/features/categories/data/providers/category_providers.dart b/lib/features/categories/data/providers/category_providers.dart new file mode 100644 index 0000000..9431808 --- /dev/null +++ b/lib/features/categories/data/providers/category_providers.dart @@ -0,0 +1,43 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:hive_ce/hive.dart'; +import '../datasources/category_local_datasource.dart'; +import '../datasources/category_remote_datasource.dart'; +import '../repositories/category_repository_impl.dart'; +import '../models/category_model.dart'; +import '../../domain/repositories/category_repository.dart'; +import '../../../../core/providers/providers.dart'; +import '../../../../core/constants/storage_constants.dart'; + +part 'category_providers.g.dart'; + +/// Provider for category Hive box +@riverpod +Box categoryBox(Ref ref) { + return Hive.box(StorageConstants.categoriesBox); +} + +/// Provider for category local data source +@riverpod +CategoryLocalDataSource categoryLocalDataSource(Ref ref) { + final box = ref.watch(categoryBoxProvider); + return CategoryLocalDataSourceImpl(box); +} + +/// Provider for category remote data source +@riverpod +CategoryRemoteDataSource categoryRemoteDataSource(Ref ref) { + final dioClient = ref.watch(dioClientProvider); + return CategoryRemoteDataSourceImpl(dioClient); +} + +/// Provider for category repository +@riverpod +CategoryRepository categoryRepository(Ref ref) { + final localDataSource = ref.watch(categoryLocalDataSourceProvider); + final remoteDataSource = ref.watch(categoryRemoteDataSourceProvider); + + return CategoryRepositoryImpl( + localDataSource: localDataSource, + remoteDataSource: remoteDataSource, + ); +} diff --git a/lib/features/categories/data/providers/category_providers.g.dart b/lib/features/categories/data/providers/category_providers.g.dart new file mode 100644 index 0000000..5ab0cdd --- /dev/null +++ b/lib/features/categories/data/providers/category_providers.g.dart @@ -0,0 +1,220 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'category_providers.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning +/// Provider for category Hive box + +@ProviderFor(categoryBox) +const categoryBoxProvider = CategoryBoxProvider._(); + +/// Provider for category Hive box + +final class CategoryBoxProvider + extends + $FunctionalProvider< + Box, + Box, + Box + > + with $Provider> { + /// Provider for category Hive box + const CategoryBoxProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'categoryBoxProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$categoryBoxHash(); + + @$internal + @override + $ProviderElement> $createElement( + $ProviderPointer pointer, + ) => $ProviderElement(pointer); + + @override + Box create(Ref ref) { + return categoryBox(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(Box value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider>(value), + ); + } +} + +String _$categoryBoxHash() => r'cbcd3cf6f0673b13a5e0af6dba10ca10f32be70c'; + +/// Provider for category local data source + +@ProviderFor(categoryLocalDataSource) +const categoryLocalDataSourceProvider = CategoryLocalDataSourceProvider._(); + +/// Provider for category local data source + +final class CategoryLocalDataSourceProvider + extends + $FunctionalProvider< + CategoryLocalDataSource, + CategoryLocalDataSource, + CategoryLocalDataSource + > + with $Provider { + /// Provider for category local data source + const CategoryLocalDataSourceProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'categoryLocalDataSourceProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$categoryLocalDataSourceHash(); + + @$internal + @override + $ProviderElement $createElement( + $ProviderPointer pointer, + ) => $ProviderElement(pointer); + + @override + CategoryLocalDataSource create(Ref ref) { + return categoryLocalDataSource(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(CategoryLocalDataSource value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$categoryLocalDataSourceHash() => + r'8d42c0dcfb986dfa0413e4267c4b08f24963ef50'; + +/// Provider for category remote data source + +@ProviderFor(categoryRemoteDataSource) +const categoryRemoteDataSourceProvider = CategoryRemoteDataSourceProvider._(); + +/// Provider for category remote data source + +final class CategoryRemoteDataSourceProvider + extends + $FunctionalProvider< + CategoryRemoteDataSource, + CategoryRemoteDataSource, + CategoryRemoteDataSource + > + with $Provider { + /// Provider for category remote data source + const CategoryRemoteDataSourceProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'categoryRemoteDataSourceProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$categoryRemoteDataSourceHash(); + + @$internal + @override + $ProviderElement $createElement( + $ProviderPointer pointer, + ) => $ProviderElement(pointer); + + @override + CategoryRemoteDataSource create(Ref ref) { + return categoryRemoteDataSource(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(CategoryRemoteDataSource value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$categoryRemoteDataSourceHash() => + r'60294160d6655f1455064fb01016d341570e9a5d'; + +/// Provider for category repository + +@ProviderFor(categoryRepository) +const categoryRepositoryProvider = CategoryRepositoryProvider._(); + +/// Provider for category repository + +final class CategoryRepositoryProvider + extends + $FunctionalProvider< + CategoryRepository, + CategoryRepository, + CategoryRepository + > + with $Provider { + /// Provider for category repository + const CategoryRepositoryProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'categoryRepositoryProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$categoryRepositoryHash(); + + @$internal + @override + $ProviderElement $createElement( + $ProviderPointer pointer, + ) => $ProviderElement(pointer); + + @override + CategoryRepository create(Ref ref) { + return categoryRepository(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(CategoryRepository value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$categoryRepositoryHash() => + r'256a9f2aa52a1858bbb50a87f2f838c33552ef22'; diff --git a/lib/features/categories/data/repositories/category_repository_impl.dart b/lib/features/categories/data/repositories/category_repository_impl.dart index 7b165b2..b1593c8 100644 --- a/lib/features/categories/data/repositories/category_repository_impl.dart +++ b/lib/features/categories/data/repositories/category_repository_impl.dart @@ -2,14 +2,17 @@ import 'package:dartz/dartz.dart'; import '../../domain/entities/category.dart'; import '../../domain/repositories/category_repository.dart'; import '../datasources/category_local_datasource.dart'; +import '../datasources/category_remote_datasource.dart'; import '../../../../core/errors/failures.dart'; import '../../../../core/errors/exceptions.dart'; class CategoryRepositoryImpl implements CategoryRepository { final CategoryLocalDataSource localDataSource; + final CategoryRemoteDataSource remoteDataSource; CategoryRepositoryImpl({ required this.localDataSource, + required this.remoteDataSource, }); @override @@ -38,12 +41,13 @@ class CategoryRepositoryImpl implements CategoryRepository { @override Future>> syncCategories() async { try { - // For now, return cached categories - // In the future, implement remote sync - final categories = await localDataSource.getAllCategories(); + final categories = await remoteDataSource.getAllCategories(); + await localDataSource.cacheCategories(categories); return Right(categories.map((model) => model.toEntity()).toList()); - } on CacheException catch (e) { - return Left(CacheFailure(e.message)); + } on ServerException catch (e) { + return Left(ServerFailure(e.message)); + } on NetworkException catch (e) { + return Left(NetworkFailure(e.message)); } } } diff --git a/lib/features/categories/presentation/pages/category_detail_page.dart b/lib/features/categories/presentation/pages/category_detail_page.dart new file mode 100644 index 0000000..55f99a5 --- /dev/null +++ b/lib/features/categories/presentation/pages/category_detail_page.dart @@ -0,0 +1,169 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../domain/entities/category.dart'; +import '../../../products/presentation/providers/products_provider.dart'; +import '../../../products/presentation/widgets/product_card.dart'; +import '../../../products/presentation/widgets/product_list_item.dart'; + +/// View mode for products display +enum ViewMode { grid, list } + +/// Category detail page showing products in the category +class CategoryDetailPage extends ConsumerStatefulWidget { + final Category category; + + const CategoryDetailPage({ + super.key, + required this.category, + }); + + @override + ConsumerState createState() => _CategoryDetailPageState(); +} + +class _CategoryDetailPageState extends ConsumerState { + ViewMode _viewMode = ViewMode.grid; + + @override + Widget build(BuildContext context) { + final productsAsync = ref.watch(productsProvider); + + return Scaffold( + appBar: AppBar( + title: Text(widget.category.name), + actions: [ + // View mode toggle + IconButton( + icon: Icon( + _viewMode == ViewMode.grid + ? Icons.view_list_rounded + : Icons.grid_view_rounded, + ), + onPressed: () { + setState(() { + _viewMode = + _viewMode == ViewMode.grid ? ViewMode.list : ViewMode.grid; + }); + }, + tooltip: _viewMode == ViewMode.grid + ? 'Switch to list view' + : 'Switch to grid view', + ), + ], + ), + body: productsAsync.when( + data: (products) { + // Filter products by category + final categoryProducts = products + .where((product) => product.categoryId == widget.category.id) + .toList(); + + if (categoryProducts.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.inventory_2_outlined, + size: 64, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + const SizedBox(height: 16), + Text( + 'No products in this category', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 8), + Text( + 'Products will appear here once added', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ); + } + + return RefreshIndicator( + onRefresh: () async { + await ref.read(productsProvider.notifier).syncProducts(); + }, + child: _viewMode == ViewMode.grid + ? _buildGridView(categoryProducts) + : _buildListView(categoryProducts), + ); + }, + loading: () => const Center( + child: CircularProgressIndicator(), + ), + error: (error, stack) => Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outline, + size: 64, + color: Theme.of(context).colorScheme.error, + ), + const SizedBox(height: 16), + Text( + 'Error loading products', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + Text( + error.toString(), + style: Theme.of(context).textTheme.bodySmall, + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + FilledButton.icon( + onPressed: () { + ref.invalidate(productsProvider); + }, + icon: const Icon(Icons.refresh), + label: const Text('Retry'), + ), + ], + ), + ), + ), + ); + } + + /// Build grid view + Widget _buildGridView(List products) { + return GridView.builder( + padding: const EdgeInsets.all(16), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 0.75, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + itemCount: products.length, + itemBuilder: (context, index) { + return ProductCard(product: products[index]); + }, + ); + } + + /// Build list view + Widget _buildListView(List products) { + return ListView.builder( + padding: const EdgeInsets.symmetric(vertical: 8), + itemCount: products.length, + itemBuilder: (context, index) { + return ProductListItem( + product: products[index], + onTap: () { + // TODO: Navigate to product detail or add to cart + }, + ); + }, + ); + } +} diff --git a/lib/features/categories/presentation/providers/categories_provider.dart b/lib/features/categories/presentation/providers/categories_provider.dart index b84406d..454c1bd 100644 --- a/lib/features/categories/presentation/providers/categories_provider.dart +++ b/lib/features/categories/presentation/providers/categories_provider.dart @@ -1,31 +1,92 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../../domain/entities/category.dart'; +import '../../data/providers/category_providers.dart'; +import '../../../../core/providers/providers.dart'; part 'categories_provider.g.dart'; -/// Provider for categories list +/// Provider for categories list with API-first approach @riverpod class Categories extends _$Categories { @override Future> build() async { - // TODO: Implement with repository - return []; + // API-first: Try to load from API first + final repository = ref.watch(categoryRepositoryProvider); + final networkInfo = ref.watch(networkInfoProvider); + + // Check if online + final isConnected = await networkInfo.isConnected; + + if (isConnected) { + // Try API first + try { + final syncResult = await repository.syncCategories(); + return syncResult.fold( + (failure) { + // API failed, fallback to cache + print('Categories API failed, falling back to cache: ${failure.message}'); + return _loadFromCache(); + }, + (categories) => categories, + ); + } catch (e) { + // API error, fallback to cache + print('Categories API error, falling back to cache: $e'); + return _loadFromCache(); + } + } else { + // Offline, load from cache + print('Offline, loading categories from cache'); + return _loadFromCache(); + } } + /// Load categories from local cache + Future> _loadFromCache() async { + final repository = ref.read(categoryRepositoryProvider); + final result = await repository.getAllCategories(); + + return result.fold( + (failure) { + print('Categories cache load failed: ${failure.message}'); + return []; + }, + (categories) => categories, + ); + } + + /// Refresh categories from local storage Future refresh() async { state = const AsyncValue.loading(); state = await AsyncValue.guard(() async { - // Fetch categories from repository - return []; + final repository = ref.read(categoryRepositoryProvider); + final result = await repository.getAllCategories(); + + return result.fold( + (failure) => throw Exception(failure.message), + (categories) => categories, + ); }); } + /// Sync categories from API and update local storage Future syncCategories() async { - // TODO: Implement sync logic with remote data source + final networkInfo = ref.read(networkInfoProvider); + final isConnected = await networkInfo.isConnected; + + if (!isConnected) { + throw Exception('No internet connection'); + } + state = const AsyncValue.loading(); state = await AsyncValue.guard(() async { - // Sync categories from API - return []; + final repository = ref.read(categoryRepositoryProvider); + final result = await repository.syncCategories(); + + return result.fold( + (failure) => throw Exception(failure.message), + (categories) => categories, + ); }); } } diff --git a/lib/features/categories/presentation/providers/categories_provider.g.dart b/lib/features/categories/presentation/providers/categories_provider.g.dart index f7a8707..8d1fc19 100644 --- a/lib/features/categories/presentation/providers/categories_provider.g.dart +++ b/lib/features/categories/presentation/providers/categories_provider.g.dart @@ -8,15 +8,15 @@ part of 'categories_provider.dart'; // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint, type=warning -/// Provider for categories list +/// Provider for categories list with API-first approach @ProviderFor(Categories) const categoriesProvider = CategoriesProvider._(); -/// Provider for categories list +/// Provider for categories list with API-first approach final class CategoriesProvider extends $AsyncNotifierProvider> { - /// Provider for categories list + /// Provider for categories list with API-first approach const CategoriesProvider._() : super( from: null, @@ -36,9 +36,9 @@ final class CategoriesProvider Categories create() => Categories(); } -String _$categoriesHash() => r'aa7afc38a5567b0f42ff05ca23b287baa4780cbe'; +String _$categoriesHash() => r'33c33b08f8926e5bbbd112285591c74a3ff0f61c'; -/// Provider for categories list +/// Provider for categories list with API-first approach abstract class _$Categories extends $AsyncNotifier> { FutureOr> build(); diff --git a/lib/features/categories/presentation/widgets/category_card.dart b/lib/features/categories/presentation/widgets/category_card.dart index f6ff295..7d9476e 100644 --- a/lib/features/categories/presentation/widgets/category_card.dart +++ b/lib/features/categories/presentation/widgets/category_card.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import '../../domain/entities/category.dart'; +import '../pages/category_detail_page.dart'; /// Category card widget class CategoryCard extends StatelessWidget { @@ -20,7 +21,13 @@ class CategoryCard extends StatelessWidget { color: color, child: InkWell( onTap: () { - // TODO: Filter products by category + // Navigate to category detail page + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CategoryDetailPage(category: category), + ), + ); }, child: Padding( padding: const EdgeInsets.all(16.0), diff --git a/lib/features/products/presentation/pages/products_page.dart b/lib/features/products/presentation/pages/products_page.dart index 3c44cf8..7363e32 100644 --- a/lib/features/products/presentation/pages/products_page.dart +++ b/lib/features/products/presentation/pages/products_page.dart @@ -2,13 +2,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../widgets/product_grid.dart'; import '../widgets/product_search_bar.dart'; +import '../widgets/product_list_item.dart'; import '../providers/products_provider.dart'; import '../providers/selected_category_provider.dart' as product_providers; import '../providers/filtered_products_provider.dart'; import '../../domain/entities/product.dart'; import '../../../categories/presentation/providers/categories_provider.dart'; -/// Products page - displays all products in a grid +/// View mode for products display +enum ViewMode { grid, list } + +/// Products page - displays all products in a grid or list class ProductsPage extends ConsumerStatefulWidget { const ProductsPage({super.key}); @@ -18,6 +22,7 @@ class ProductsPage extends ConsumerStatefulWidget { class _ProductsPageState extends ConsumerState { ProductSortOption _sortOption = ProductSortOption.nameAsc; + ViewMode _viewMode = ViewMode.grid; @override Widget build(BuildContext context) { @@ -43,6 +48,23 @@ class _ProductsPageState extends ConsumerState { appBar: AppBar( title: const Text('Products'), actions: [ + // View mode toggle + IconButton( + icon: Icon( + _viewMode == ViewMode.grid + ? Icons.view_list_rounded + : Icons.grid_view_rounded, + ), + onPressed: () { + setState(() { + _viewMode = + _viewMode == ViewMode.grid ? ViewMode.list : ViewMode.grid; + }); + }, + tooltip: _viewMode == ViewMode.grid + ? 'Switch to list view' + : 'Switch to grid view', + ), // Sort button PopupMenuButton( icon: const Icon(Icons.sort), @@ -195,11 +217,13 @@ class _ProductsPageState extends ConsumerState { ), ), ), - // Product grid + // Product grid or list Expanded( - child: ProductGrid( - sortOption: _sortOption, - ), + child: _viewMode == ViewMode.grid + ? ProductGrid( + sortOption: _sortOption, + ) + : _buildListView(), ), ], ), @@ -223,4 +247,84 @@ class _ProductsPageState extends ConsumerState { ), ); } + + /// Build list view for products + Widget _buildListView() { + final filteredProducts = ref.watch(filteredProductsProvider); + + // Apply sorting + final sortedProducts = _sortProducts(filteredProducts, _sortOption); + + if (sortedProducts.isEmpty) { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.inventory_2_outlined, + size: 64, + color: Colors.grey, + ), + SizedBox(height: 16), + Text( + 'No products found', + style: TextStyle( + fontSize: 16, + color: Colors.grey, + ), + ), + SizedBox(height: 8), + Text( + 'Try adjusting your filters', + style: TextStyle( + fontSize: 14, + color: Colors.grey, + ), + ), + ], + ), + ); + } + + return ListView.builder( + padding: const EdgeInsets.symmetric(vertical: 8), + itemCount: sortedProducts.length, + itemBuilder: (context, index) { + return ProductListItem( + product: sortedProducts[index], + onTap: () { + // TODO: Navigate to product detail or add to cart + }, + ); + }, + ); + } + + /// Sort products based on selected option + List _sortProducts(List products, ProductSortOption option) { + final sorted = List.from(products); + + switch (option) { + case ProductSortOption.nameAsc: + sorted.sort((a, b) => a.name.compareTo(b.name)); + break; + case ProductSortOption.nameDesc: + sorted.sort((a, b) => b.name.compareTo(a.name)); + break; + case ProductSortOption.priceAsc: + sorted.sort((a, b) => a.price.compareTo(b.price)); + break; + case ProductSortOption.priceDesc: + sorted.sort((a, b) => b.price.compareTo(a.price)); + break; + case ProductSortOption.newest: + sorted.sort((a, b) => b.createdAt.compareTo(a.createdAt)); + break; + case ProductSortOption.oldest: + sorted.sort((a, b) => a.createdAt.compareTo(b.createdAt)); + break; + } + + return sorted; + } } diff --git a/lib/features/products/presentation/providers/products_provider.dart b/lib/features/products/presentation/providers/products_provider.dart index 67f64c2..ebfd8a7 100644 --- a/lib/features/products/presentation/providers/products_provider.dart +++ b/lib/features/products/presentation/providers/products_provider.dart @@ -101,17 +101,3 @@ class SearchQuery extends _$SearchQuery { state = query; } } - -/// Provider for filtered products -@riverpod -List filteredProducts(Ref ref) { - final products = ref.watch(productsProvider).value ?? []; - final query = ref.watch(searchQueryProvider); - - if (query.isEmpty) return products; - - return products.where((p) => - p.name.toLowerCase().contains(query.toLowerCase()) || - p.description.toLowerCase().contains(query.toLowerCase()) - ).toList(); -} diff --git a/lib/features/products/presentation/providers/products_provider.g.dart b/lib/features/products/presentation/providers/products_provider.g.dart index 570aa5f..1f390f9 100644 --- a/lib/features/products/presentation/providers/products_provider.g.dart +++ b/lib/features/products/presentation/providers/products_provider.g.dart @@ -116,49 +116,3 @@ abstract class _$SearchQuery extends $Notifier { element.handleValue(ref, created); } } - -/// Provider for filtered products - -@ProviderFor(filteredProducts) -const filteredProductsProvider = FilteredProductsProvider._(); - -/// Provider for filtered products - -final class FilteredProductsProvider - extends $FunctionalProvider, List, List> - with $Provider> { - /// Provider for filtered products - const FilteredProductsProvider._() - : super( - from: null, - argument: null, - retry: null, - name: r'filteredProductsProvider', - isAutoDispose: true, - dependencies: null, - $allTransitiveDependencies: null, - ); - - @override - String debugGetCreateSourceHash() => _$filteredProductsHash(); - - @$internal - @override - $ProviderElement> $createElement($ProviderPointer pointer) => - $ProviderElement(pointer); - - @override - List create(Ref ref) { - return filteredProducts(ref); - } - - /// {@macro riverpod.override_with_value} - Override overrideWithValue(List value) { - return $ProviderOverride( - origin: this, - providerOverride: $SyncValueProvider>(value), - ); - } -} - -String _$filteredProductsHash() => r'e4e0c549c454576fc599713a5237435a8dd4b277'; diff --git a/lib/features/products/presentation/widgets/product_list_item.dart b/lib/features/products/presentation/widgets/product_list_item.dart new file mode 100644 index 0000000..9f86958 --- /dev/null +++ b/lib/features/products/presentation/widgets/product_list_item.dart @@ -0,0 +1,131 @@ +import 'package:flutter/material.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import '../../domain/entities/product.dart'; +import '../../../../shared/widgets/price_display.dart'; + +/// Product list item widget for list view +class ProductListItem extends StatelessWidget { + final Product product; + final VoidCallback? onTap; + + const ProductListItem({ + super.key, + required this.product, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Card( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + children: [ + // Product Image + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: SizedBox( + width: 80, + height: 80, + child: product.imageUrl != null + ? CachedNetworkImage( + imageUrl: product.imageUrl!, + fit: BoxFit.cover, + placeholder: (context, url) => const Center( + child: CircularProgressIndicator(), + ), + errorWidget: (context, url, error) => Container( + color: Theme.of(context) + .colorScheme + .surfaceContainerHighest, + child: Icon( + Icons.image_not_supported, + color: + Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ) + : Container( + color: Theme.of(context) + .colorScheme + .surfaceContainerHighest, + child: Icon( + Icons.inventory_2_outlined, + color: + Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + ), + const SizedBox(width: 16), + // Product Info + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + product.name, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + if (product.description.isNotEmpty) + Text( + product.description, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 8), + Row( + children: [ + PriceDisplay(price: product.price), + const Spacer(), + // Stock Badge + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: _getStockColor(context, product.stockQuantity), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + 'Stock: ${product.stockQuantity}', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ); + } + + Color _getStockColor(BuildContext context, int stock) { + if (stock == 0) { + return Colors.red; + } else if (stock < 5) { + return Colors.orange; + } else { + return Colors.green; + } + } +}