# Conflicts:
#	docs/API_RESPONSE_FIX.md
#	docs/AUTH_UI_SUMMARY.md
#	docs/AUTO_LOGIN_DEBUG.md
#	docs/AUTO_LOGIN_FIXED.md
#	docs/BUILD_STATUS.md
#	docs/CLEANUP_COMPLETE.md
#	docs/EXPORT_FILES_SUMMARY.md
#	docs/RIVERPOD_DI_MIGRATION.md
#	docs/TEST_AUTO_LOGIN.md
#	lib/features/categories/data/datasources/category_remote_datasource.dart
#	lib/features/categories/presentation/providers/categories_provider.dart
#	lib/features/categories/presentation/providers/categories_provider.g.dart
#	lib/features/products/data/datasources/product_remote_datasource.dart
#	lib/features/products/data/models/product_model.dart
#	lib/features/products/presentation/pages/products_page.dart
#	lib/features/products/presentation/providers/products_provider.dart
#	lib/features/products/presentation/providers/products_provider.g.dart
This commit is contained in:
2025-10-15 20:55:40 +07:00
39 changed files with 6344 additions and 1714 deletions

View File

@@ -1,25 +1,12 @@
import 'package:dio/dio.dart';
import '../models/category_model.dart';
import '../../../../core/network/dio_client.dart';
import '../../../../core/network/api_response.dart';
import '../../../../core/constants/api_constants.dart';
import '../../../../core/errors/exceptions.dart';
/// Category remote data source using API
abstract class CategoryRemoteDataSource {
/// Get all categories (public endpoint - no auth required)
Future<List<CategoryModel>> getAllCategories();
/// Get single category by ID (public endpoint - no auth required)
Future<CategoryModel> getCategoryById(String id);
/// Get category with its products with pagination (public endpoint)
/// Returns Map with 'category' and 'products' with pagination info
Future<Map<String, dynamic>> getCategoryWithProducts(
String id,
int page,
int limit,
);
}
class CategoryRemoteDataSourceImpl implements CategoryRemoteDataSource {
@@ -32,24 +19,15 @@ class CategoryRemoteDataSourceImpl implements CategoryRemoteDataSource {
try {
final response = await client.get(ApiConstants.categories);
// Parse API response using ApiResponse model
final apiResponse = ApiResponse<List<CategoryModel>>.fromJson(
response.data as Map<String, dynamic>,
(data) => (data as List<dynamic>)
.map((json) => CategoryModel.fromJson(json as Map<String, dynamic>))
.toList(),
);
if (!apiResponse.success) {
throw ServerException(
apiResponse.message ?? 'Failed to fetch categories',
);
// API returns: { success: true, data: [...categories...] }
if (response.data['success'] == true) {
final List<dynamic> data = response.data['data'] ?? [];
return data.map((json) => CategoryModel.fromJson(json)).toList();
} else {
throw ServerException(response.data['message'] ?? 'Failed to fetch categories');
}
return apiResponse.data;
} on DioException catch (e) {
throw _handleDioError(e);
} catch (e) {
if (e is ServerException) rethrow;
throw ServerException('Failed to fetch categories: $e');
}
}
@@ -59,108 +37,15 @@ class CategoryRemoteDataSourceImpl implements CategoryRemoteDataSource {
try {
final response = await client.get(ApiConstants.categoryById(id));
// Parse API response using ApiResponse model
final apiResponse = ApiResponse<CategoryModel>.fromJson(
response.data as Map<String, dynamic>,
(data) => CategoryModel.fromJson(data as Map<String, dynamic>),
);
if (!apiResponse.success) {
throw ServerException(
apiResponse.message ?? 'Failed to fetch category',
);
// 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');
}
return apiResponse.data;
} on DioException catch (e) {
throw _handleDioError(e);
} catch (e) {
if (e is ServerException) rethrow;
throw ServerException('Failed to fetch category: $e');
}
}
@override
Future<Map<String, dynamic>> getCategoryWithProducts(
String id,
int page,
int limit,
) async {
try {
final response = await client.get(
'${ApiConstants.categories}/$id/products',
queryParameters: {
'page': page,
'limit': limit,
},
);
// Parse API response - data contains category with nested products
final apiResponse = ApiResponse<Map<String, dynamic>>.fromJson(
response.data as Map<String, dynamic>,
(data) => data as Map<String, dynamic>,
);
if (!apiResponse.success) {
throw ServerException(
apiResponse.message ?? 'Failed to fetch category with products',
);
}
final responseData = apiResponse.data;
// Extract category info (excluding products array)
final categoryData = Map<String, dynamic>.from(responseData);
final products = categoryData.remove('products') as List<dynamic>? ?? [];
// Create category model from remaining data
final category = CategoryModel.fromJson(categoryData);
return {
'category': category,
'products': products,
'meta': apiResponse.meta?.toJson() ?? {},
};
} on DioException catch (e) {
throw _handleDioError(e);
} catch (e) {
throw ServerException('Failed to fetch category with products: $e');
}
}
/// Handle Dio errors and convert to custom exceptions
Exception _handleDioError(DioException error) {
switch (error.response?.statusCode) {
case ApiConstants.statusBadRequest:
return ValidationException(
error.response?.data['message'] ?? 'Invalid request',
);
case ApiConstants.statusUnauthorized:
return UnauthorizedException(
error.response?.data['message'] ?? 'Unauthorized access',
);
case ApiConstants.statusForbidden:
return UnauthorizedException(
error.response?.data['message'] ?? 'Access forbidden',
);
case ApiConstants.statusNotFound:
return NotFoundException(
error.response?.data['message'] ?? 'Category not found',
);
case ApiConstants.statusInternalServerError:
case ApiConstants.statusBadGateway:
case ApiConstants.statusServiceUnavailable:
return ServerException(
error.response?.data['message'] ?? 'Server error',
);
default:
if (error.type == DioExceptionType.connectionTimeout ||
error.type == DioExceptionType.receiveTimeout ||
error.type == DioExceptionType.sendTimeout) {
return NetworkException('Connection timeout');
} else if (error.type == DioExceptionType.connectionError) {
return NetworkException('No internet connection');
}
return ServerException('Unexpected error occurred');
}
}
}

View File

@@ -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<CategoryModel> categoryBox(Ref ref) {
return Hive.box<CategoryModel>(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,
);
}

View File

@@ -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<CategoryModel>,
Box<CategoryModel>,
Box<CategoryModel>
>
with $Provider<Box<CategoryModel>> {
/// 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<Box<CategoryModel>> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
Box<CategoryModel> create(Ref ref) {
return categoryBox(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(Box<CategoryModel> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<Box<CategoryModel>>(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<CategoryLocalDataSource> {
/// 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<CategoryLocalDataSource> $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<CategoryLocalDataSource>(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<CategoryRemoteDataSource> {
/// 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<CategoryRemoteDataSource> $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<CategoryRemoteDataSource>(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<CategoryRepository> {
/// 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<CategoryRepository> $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<CategoryRepository>(value),
);
}
}
String _$categoryRepositoryHash() =>
r'256a9f2aa52a1858bbb50a87f2f838c33552ef22';

View File

@@ -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<Either<Failure, List<Category>>> 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));
}
}
}

View File

@@ -0,0 +1,166 @@
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<CategoryDetailPage> createState() => _CategoryDetailPageState();
}
class _CategoryDetailPageState extends ConsumerState<CategoryDetailPage> {
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],
);
},
);
}
}

View File

@@ -1,193 +1,101 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../domain/entities/category.dart';
import '../../data/models/category_model.dart';
import '../../../products/data/models/product_model.dart';
import '../../../products/domain/entities/product.dart';
import 'category_remote_datasource_provider.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<List<Category>> build() async {
return await _fetchCategories();
}
// API-first: Try to load from API first
final repository = ref.watch(categoryRepositoryProvider);
final networkInfo = ref.watch(networkInfoProvider);
Future<List<Category>> _fetchCategories() async {
final datasource = ref.read(categoryRemoteDataSourceProvider);
final categoryModels = await datasource.getAllCategories();
return categoryModels.map((model) => model.toEntity()).toList();
}
// Check if online
final isConnected = await networkInfo.isConnected;
Future<void> refresh() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
return await _fetchCategories();
});
}
}
/// Provider for single category by ID
@riverpod
Future<Category> category(Ref ref, String id) async {
final datasource = ref.read(categoryRemoteDataSourceProvider);
final categoryModel = await datasource.getCategoryById(id);
return categoryModel.toEntity();
}
/// Pagination state for category products
class CategoryProductsState {
final Category category;
final List<Product> products;
final int currentPage;
final int totalPages;
final int totalItems;
final bool hasMore;
final bool isLoadingMore;
const CategoryProductsState({
required this.category,
required this.products,
required this.currentPage,
required this.totalPages,
required this.totalItems,
required this.hasMore,
this.isLoadingMore = false,
});
CategoryProductsState copyWith({
Category? category,
List<Product>? products,
int? currentPage,
int? totalPages,
int? totalItems,
bool? hasMore,
bool? isLoadingMore,
}) {
return CategoryProductsState(
category: category ?? this.category,
products: products ?? this.products,
currentPage: currentPage ?? this.currentPage,
totalPages: totalPages ?? this.totalPages,
totalItems: totalItems ?? this.totalItems,
hasMore: hasMore ?? this.hasMore,
isLoadingMore: isLoadingMore ?? this.isLoadingMore,
);
}
}
/// Provider for category with its products (with pagination)
@riverpod
class CategoryWithProducts extends _$CategoryWithProducts {
static const int _limit = 20;
@override
Future<CategoryProductsState> build(String categoryId) async {
return await _fetchCategoryWithProducts(categoryId: categoryId, page: 1);
}
Future<CategoryProductsState> _fetchCategoryWithProducts({
required String categoryId,
required int page,
}) async {
final datasource = ref.read(categoryRemoteDataSourceProvider);
final response = await datasource.getCategoryWithProducts(
categoryId,
page,
_limit,
);
// Extract data
final CategoryModel categoryModel = response['category'] as CategoryModel;
final List<dynamic> productsJson = response['products'] as List<dynamic>;
final meta = response['meta'] as Map<String, dynamic>;
// Convert category to entity
final category = categoryModel.toEntity();
// Convert products to entities
final products = productsJson
.map((json) => ProductModel.fromJson(json as Map<String, dynamic>))
.map((model) => model.toEntity())
.toList();
// Extract pagination info
final currentPage = meta['currentPage'] as int? ?? page;
final totalPages = meta['totalPages'] as int? ?? 1;
final totalItems = meta['totalItems'] as int? ?? products.length;
final hasMore = currentPage < totalPages;
return CategoryProductsState(
category: category,
products: products,
currentPage: currentPage,
totalPages: totalPages,
totalItems: totalItems,
hasMore: hasMore,
);
}
/// Load more products (next page)
Future<void> loadMore() async {
final currentState = state.value;
if (currentState == null || !currentState.hasMore) return;
// Set loading more flag
state = AsyncValue.data(
currentState.copyWith(isLoadingMore: true),
);
// Fetch next page
final nextPage = currentState.currentPage + 1;
try {
final newState = await _fetchCategoryWithProducts(
categoryId: currentState.category.id,
page: nextPage,
);
// Append new products to existing ones
state = AsyncValue.data(
newState.copyWith(
products: [...currentState.products, ...newState.products],
isLoadingMore: false,
),
);
} catch (error, stackTrace) {
// Restore previous state on error
state = AsyncValue.data(
currentState.copyWith(isLoadingMore: false),
);
state = AsyncValue.error(error, stackTrace);
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();
}
}
/// Refresh category and products
/// Load categories from local cache
Future<List<Category>> _loadFromCache() async {
final repository = ref.read(categoryRepositoryProvider);
final result = await repository.getAllCategories();
return result.fold(
(failure) {
print('Categories cache load failed: ${failure.message}');
return <Category>[];
},
(categories) => categories,
);
}
/// Refresh categories from local storage
Future<void> refresh() async {
final currentState = state.value;
if (currentState == null) return;
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
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<void> syncCategories() async {
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 {
return await _fetchCategoryWithProducts(
categoryId: currentState.category.id,
page: 1,
final repository = ref.read(categoryRepositoryProvider);
final result = await repository.syncCategories();
return result.fold(
(failure) => throw Exception(failure.message),
(categories) => categories,
);
});
}
}
/// Provider for selected category state
/// This is used in the products feature for filtering
/// Provider for selected category
@riverpod
class SelectedCategoryInCategories extends _$SelectedCategoryInCategories {
class SelectedCategory extends _$SelectedCategory {
@override
String? build() {
return null;
}
String? build() => null;
void select(String? categoryId) {
state = categoryId;
@@ -196,8 +104,4 @@ class SelectedCategoryInCategories extends _$SelectedCategoryInCategories {
void clear() {
state = null;
}
bool get hasSelection => state != null;
bool isSelected(String categoryId) => state == categoryId;
}

View File

@@ -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<Categories, List<Category>> {
/// 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'5156d31a6d7b9457c4735b66e170b262140758e2';
String _$categoriesHash() => r'33c33b08f8926e5bbbd112285591c74a3ff0f61c';
/// Provider for categories list
/// Provider for categories list with API-first approach
abstract class _$Categories extends $AsyncNotifier<List<Category>> {
FutureOr<List<Category>> build();
@@ -59,223 +59,32 @@ abstract class _$Categories extends $AsyncNotifier<List<Category>> {
}
}
/// Provider for single category by ID
/// Provider for selected category
@ProviderFor(category)
const categoryProvider = CategoryFamily._();
@ProviderFor(SelectedCategory)
const selectedCategoryProvider = SelectedCategoryProvider._();
/// Provider for single category by ID
final class CategoryProvider
extends
$FunctionalProvider<AsyncValue<Category>, Category, FutureOr<Category>>
with $FutureModifier<Category>, $FutureProvider<Category> {
/// Provider for single category by ID
const CategoryProvider._({
required CategoryFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'categoryProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$categoryHash();
@override
String toString() {
return r'categoryProvider'
''
'($argument)';
}
@$internal
@override
$FutureProviderElement<Category> $createElement($ProviderPointer pointer) =>
$FutureProviderElement(pointer);
@override
FutureOr<Category> create(Ref ref) {
final argument = this.argument as String;
return category(ref, argument);
}
@override
bool operator ==(Object other) {
return other is CategoryProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$categoryHash() => r'e26dd362e42a1217a774072f453a64c7a6195e73';
/// Provider for single category by ID
final class CategoryFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<Category>, String> {
const CategoryFamily._()
: super(
retry: null,
name: r'categoryProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
/// Provider for single category by ID
CategoryProvider call(String id) =>
CategoryProvider._(argument: id, from: this);
@override
String toString() => r'categoryProvider';
}
/// Provider for category with its products (with pagination)
@ProviderFor(CategoryWithProducts)
const categoryWithProductsProvider = CategoryWithProductsFamily._();
/// Provider for category with its products (with pagination)
final class CategoryWithProductsProvider
extends
$AsyncNotifierProvider<CategoryWithProducts, CategoryProductsState> {
/// Provider for category with its products (with pagination)
const CategoryWithProductsProvider._({
required CategoryWithProductsFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'categoryWithProductsProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$categoryWithProductsHash();
@override
String toString() {
return r'categoryWithProductsProvider'
''
'($argument)';
}
@$internal
@override
CategoryWithProducts create() => CategoryWithProducts();
@override
bool operator ==(Object other) {
return other is CategoryWithProductsProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$categoryWithProductsHash() =>
r'a5ea35fad4e711ea855e4874f9135145d7d44b67';
/// Provider for category with its products (with pagination)
final class CategoryWithProductsFamily extends $Family
with
$ClassFamilyOverride<
CategoryWithProducts,
AsyncValue<CategoryProductsState>,
CategoryProductsState,
FutureOr<CategoryProductsState>,
String
> {
const CategoryWithProductsFamily._()
: super(
retry: null,
name: r'categoryWithProductsProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
/// Provider for category with its products (with pagination)
CategoryWithProductsProvider call(String categoryId) =>
CategoryWithProductsProvider._(argument: categoryId, from: this);
@override
String toString() => r'categoryWithProductsProvider';
}
/// Provider for category with its products (with pagination)
abstract class _$CategoryWithProducts
extends $AsyncNotifier<CategoryProductsState> {
late final _$args = ref.$arg as String;
String get categoryId => _$args;
FutureOr<CategoryProductsState> build(String categoryId);
@$mustCallSuper
@override
void runBuild() {
final created = build(_$args);
final ref =
this.ref
as $Ref<AsyncValue<CategoryProductsState>, CategoryProductsState>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<
AsyncValue<CategoryProductsState>,
CategoryProductsState
>,
AsyncValue<CategoryProductsState>,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
/// Provider for selected category state
/// This is used in the products feature for filtering
@ProviderFor(SelectedCategoryInCategories)
const selectedCategoryInCategoriesProvider =
SelectedCategoryInCategoriesProvider._();
/// Provider for selected category state
/// This is used in the products feature for filtering
final class SelectedCategoryInCategoriesProvider
extends $NotifierProvider<SelectedCategoryInCategories, String?> {
/// Provider for selected category state
/// This is used in the products feature for filtering
const SelectedCategoryInCategoriesProvider._()
/// Provider for selected category
final class SelectedCategoryProvider
extends $NotifierProvider<SelectedCategory, String?> {
/// Provider for selected category
const SelectedCategoryProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'selectedCategoryInCategoriesProvider',
name: r'selectedCategoryProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$selectedCategoryInCategoriesHash();
String debugGetCreateSourceHash() => _$selectedCategoryHash();
@$internal
@override
SelectedCategoryInCategories create() => SelectedCategoryInCategories();
SelectedCategory create() => SelectedCategory();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(String? value) {
@@ -286,13 +95,11 @@ final class SelectedCategoryInCategoriesProvider
}
}
String _$selectedCategoryInCategoriesHash() =>
r'510d79a73dcfeba5efa886f5f95f7470dbd09a47';
String _$selectedCategoryHash() => r'a47cd2de07ad285d4b73b2294ba954cb1cdd8e4c';
/// Provider for selected category state
/// This is used in the products feature for filtering
/// Provider for selected category
abstract class _$SelectedCategoryInCategories extends $Notifier<String?> {
abstract class _$SelectedCategory extends $Notifier<String?> {
String? build();
@$mustCallSuper
@override

View File

@@ -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),