runable
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import '../models/category_model.dart';
|
||||
|
||||
/// Category local data source using Hive
|
||||
abstract class CategoryLocalDataSource {
|
||||
Future<List<CategoryModel>> getAllCategories();
|
||||
Future<CategoryModel?> getCategoryById(String id);
|
||||
Future<void> cacheCategories(List<CategoryModel> categories);
|
||||
Future<void> clearCategories();
|
||||
}
|
||||
|
||||
class CategoryLocalDataSourceImpl implements CategoryLocalDataSource {
|
||||
final Box<CategoryModel> box;
|
||||
|
||||
CategoryLocalDataSourceImpl(this.box);
|
||||
|
||||
@override
|
||||
Future<List<CategoryModel>> getAllCategories() async {
|
||||
return box.values.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CategoryModel?> getCategoryById(String id) async {
|
||||
return box.get(id);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> cacheCategories(List<CategoryModel> categories) async {
|
||||
final categoryMap = {for (var c in categories) c.id: c};
|
||||
await box.putAll(categoryMap);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clearCategories() async {
|
||||
await box.clear();
|
||||
}
|
||||
}
|
||||
112
lib/features/categories/data/models/category_model.dart
Normal file
112
lib/features/categories/data/models/category_model.dart
Normal file
@@ -0,0 +1,112 @@
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import '../../domain/entities/category.dart';
|
||||
import '../../../../core/constants/storage_constants.dart';
|
||||
|
||||
part 'category_model.g.dart';
|
||||
|
||||
@HiveType(typeId: StorageConstants.categoryTypeId)
|
||||
class CategoryModel extends HiveObject {
|
||||
@HiveField(0)
|
||||
final String id;
|
||||
|
||||
@HiveField(1)
|
||||
final String name;
|
||||
|
||||
@HiveField(2)
|
||||
final String? description;
|
||||
|
||||
@HiveField(3)
|
||||
final String? iconPath;
|
||||
|
||||
@HiveField(4)
|
||||
final String? color;
|
||||
|
||||
@HiveField(5)
|
||||
final int productCount;
|
||||
|
||||
@HiveField(6)
|
||||
final DateTime createdAt;
|
||||
|
||||
CategoryModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.description,
|
||||
this.iconPath,
|
||||
this.color,
|
||||
required this.productCount,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
/// Convert to domain entity
|
||||
Category toEntity() {
|
||||
return Category(
|
||||
id: id,
|
||||
name: name,
|
||||
description: description,
|
||||
iconPath: iconPath,
|
||||
color: color,
|
||||
productCount: productCount,
|
||||
createdAt: createdAt,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create from domain entity
|
||||
factory CategoryModel.fromEntity(Category category) {
|
||||
return CategoryModel(
|
||||
id: category.id,
|
||||
name: category.name,
|
||||
description: category.description,
|
||||
iconPath: category.iconPath,
|
||||
color: category.color,
|
||||
productCount: category.productCount,
|
||||
createdAt: category.createdAt,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create from JSON
|
||||
factory CategoryModel.fromJson(Map<String, dynamic> json) {
|
||||
return CategoryModel(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String?,
|
||||
iconPath: json['iconPath'] as String?,
|
||||
color: json['color'] as String?,
|
||||
productCount: json['productCount'] as int,
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert to JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'description': description,
|
||||
'iconPath': iconPath,
|
||||
'color': color,
|
||||
'productCount': productCount,
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Create a copy with updated fields
|
||||
CategoryModel copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
String? description,
|
||||
String? iconPath,
|
||||
String? color,
|
||||
int? productCount,
|
||||
DateTime? createdAt,
|
||||
}) {
|
||||
return CategoryModel(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
description: description ?? this.description,
|
||||
iconPath: iconPath ?? this.iconPath,
|
||||
color: color ?? this.color,
|
||||
productCount: productCount ?? this.productCount,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
59
lib/features/categories/data/models/category_model.g.dart
Normal file
59
lib/features/categories/data/models/category_model.g.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'category_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class CategoryModelAdapter extends TypeAdapter<CategoryModel> {
|
||||
@override
|
||||
final typeId = 1;
|
||||
|
||||
@override
|
||||
CategoryModel read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return CategoryModel(
|
||||
id: fields[0] as String,
|
||||
name: fields[1] as String,
|
||||
description: fields[2] as String?,
|
||||
iconPath: fields[3] as String?,
|
||||
color: fields[4] as String?,
|
||||
productCount: (fields[5] as num).toInt(),
|
||||
createdAt: fields[6] as DateTime,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, CategoryModel obj) {
|
||||
writer
|
||||
..writeByte(7)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
..write(obj.name)
|
||||
..writeByte(2)
|
||||
..write(obj.description)
|
||||
..writeByte(3)
|
||||
..write(obj.iconPath)
|
||||
..writeByte(4)
|
||||
..write(obj.color)
|
||||
..writeByte(5)
|
||||
..write(obj.productCount)
|
||||
..writeByte(6)
|
||||
..write(obj.createdAt);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is CategoryModelAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../domain/entities/category.dart';
|
||||
import '../../domain/repositories/category_repository.dart';
|
||||
import '../datasources/category_local_datasource.dart';
|
||||
import '../../../../core/errors/failures.dart';
|
||||
import '../../../../core/errors/exceptions.dart';
|
||||
|
||||
class CategoryRepositoryImpl implements CategoryRepository {
|
||||
final CategoryLocalDataSource localDataSource;
|
||||
|
||||
CategoryRepositoryImpl({
|
||||
required this.localDataSource,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<Category>>> getAllCategories() async {
|
||||
try {
|
||||
final categories = await localDataSource.getAllCategories();
|
||||
return Right(categories.map((model) => model.toEntity()).toList());
|
||||
} on CacheException catch (e) {
|
||||
return Left(CacheFailure(e.message));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Category>> getCategoryById(String id) async {
|
||||
try {
|
||||
final category = await localDataSource.getCategoryById(id);
|
||||
if (category == null) {
|
||||
return Left(NotFoundFailure('Category not found'));
|
||||
}
|
||||
return Right(category.toEntity());
|
||||
} on CacheException catch (e) {
|
||||
return Left(CacheFailure(e.message));
|
||||
}
|
||||
}
|
||||
|
||||
@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();
|
||||
return Right(categories.map((model) => model.toEntity()).toList());
|
||||
} on CacheException catch (e) {
|
||||
return Left(CacheFailure(e.message));
|
||||
}
|
||||
}
|
||||
}
|
||||
33
lib/features/categories/domain/entities/category.dart
Normal file
33
lib/features/categories/domain/entities/category.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Category domain entity
|
||||
class Category extends Equatable {
|
||||
final String id;
|
||||
final String name;
|
||||
final String? description;
|
||||
final String? iconPath;
|
||||
final String? color;
|
||||
final int productCount;
|
||||
final DateTime createdAt;
|
||||
|
||||
const Category({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.description,
|
||||
this.iconPath,
|
||||
this.color,
|
||||
required this.productCount,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
iconPath,
|
||||
color,
|
||||
productCount,
|
||||
createdAt,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../../../core/errors/failures.dart';
|
||||
import '../entities/category.dart';
|
||||
|
||||
/// Category repository interface
|
||||
abstract class CategoryRepository {
|
||||
/// Get all categories from cache
|
||||
Future<Either<Failure, List<Category>>> getAllCategories();
|
||||
|
||||
/// Get category by ID
|
||||
Future<Either<Failure, Category>> getCategoryById(String id);
|
||||
|
||||
/// Sync categories from remote
|
||||
Future<Either<Failure, List<Category>>> syncCategories();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../../../core/errors/failures.dart';
|
||||
import '../entities/category.dart';
|
||||
import '../repositories/category_repository.dart';
|
||||
|
||||
/// Use case to get all categories
|
||||
class GetAllCategories {
|
||||
final CategoryRepository repository;
|
||||
|
||||
GetAllCategories(this.repository);
|
||||
|
||||
Future<Either<Failure, List<Category>>> call() async {
|
||||
return await repository.getAllCategories();
|
||||
}
|
||||
}
|
||||
116
lib/features/categories/presentation/pages/categories_page.dart
Normal file
116
lib/features/categories/presentation/pages/categories_page.dart
Normal file
@@ -0,0 +1,116 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../widgets/category_grid.dart';
|
||||
import '../providers/categories_provider.dart';
|
||||
import '../../../products/presentation/providers/selected_category_provider.dart' as product_providers;
|
||||
|
||||
/// Categories page - displays all categories in a grid
|
||||
class CategoriesPage extends ConsumerWidget {
|
||||
const CategoriesPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final categoriesAsync = ref.watch(categoriesProvider);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Categories'),
|
||||
actions: [
|
||||
// Refresh button
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
tooltip: 'Refresh categories',
|
||||
onPressed: () {
|
||||
ref.invalidate(categoriesProvider);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await ref.refresh(categoriesProvider.future);
|
||||
},
|
||||
child: categoriesAsync.when(
|
||||
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 categories',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
error.toString(),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => ref.invalidate(categoriesProvider),
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Retry'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
data: (categories) {
|
||||
return Column(
|
||||
children: [
|
||||
// Categories count
|
||||
if (categories.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'${categories.length} categor${categories.length == 1 ? 'y' : 'ies'}',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Category grid
|
||||
Expanded(
|
||||
child: CategoryGrid(
|
||||
onCategoryTap: (category) {
|
||||
// Set selected category
|
||||
ref
|
||||
.read(product_providers.selectedCategoryProvider.notifier)
|
||||
.selectCategory(category.id);
|
||||
|
||||
// Show snackbar
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Filtering products by ${category.name}',
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
action: SnackBarAction(
|
||||
label: 'View',
|
||||
onPressed: () {
|
||||
// Navigate to products tab
|
||||
// This will be handled by the parent widget
|
||||
// For now, just show a message
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import '../../domain/entities/category.dart';
|
||||
|
||||
part 'categories_provider.g.dart';
|
||||
|
||||
/// Provider for categories list
|
||||
@riverpod
|
||||
class Categories extends _$Categories {
|
||||
@override
|
||||
Future<List<Category>> build() async {
|
||||
// TODO: Implement with repository
|
||||
return [];
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
// Fetch categories from repository
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> syncCategories() async {
|
||||
// TODO: Implement sync logic with remote data source
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
// Sync categories from API
|
||||
return [];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider for selected category
|
||||
@riverpod
|
||||
class SelectedCategory extends _$SelectedCategory {
|
||||
@override
|
||||
String? build() => null;
|
||||
|
||||
void select(String? categoryId) {
|
||||
state = categoryId;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
state = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// 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
|
||||
/// Provider for categories list
|
||||
|
||||
@ProviderFor(Categories)
|
||||
const categoriesProvider = CategoriesProvider._();
|
||||
|
||||
/// Provider for categories list
|
||||
final class CategoriesProvider
|
||||
extends $AsyncNotifierProvider<Categories, List<Category>> {
|
||||
/// Provider for categories list
|
||||
const CategoriesProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'categoriesProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$categoriesHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
Categories create() => Categories();
|
||||
}
|
||||
|
||||
String _$categoriesHash() => r'aa7afc38a5567b0f42ff05ca23b287baa4780cbe';
|
||||
|
||||
/// Provider for categories list
|
||||
|
||||
abstract class _$Categories extends $AsyncNotifier<List<Category>> {
|
||||
FutureOr<List<Category>> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<AsyncValue<List<Category>>, List<Category>>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<AsyncValue<List<Category>>, List<Category>>,
|
||||
AsyncValue<List<Category>>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider for selected category
|
||||
|
||||
@ProviderFor(SelectedCategory)
|
||||
const selectedCategoryProvider = SelectedCategoryProvider._();
|
||||
|
||||
/// 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'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'a47cd2de07ad285d4b73b2294ba954cb1cdd8e4c';
|
||||
|
||||
/// Provider for selected category
|
||||
|
||||
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,14 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import '../../data/datasources/category_local_datasource.dart';
|
||||
import '../../../../core/database/hive_database.dart';
|
||||
import '../../data/models/category_model.dart';
|
||||
|
||||
part 'category_datasource_provider.g.dart';
|
||||
|
||||
/// Provider for category local data source
|
||||
/// This is kept alive as it's a dependency injection provider
|
||||
@Riverpod(keepAlive: true)
|
||||
CategoryLocalDataSource categoryLocalDataSource(Ref ref) {
|
||||
final box = HiveDatabase.instance.getBox<CategoryModel>('categories');
|
||||
return CategoryLocalDataSourceImpl(box);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'category_datasource_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Provider for category local data source
|
||||
/// This is kept alive as it's a dependency injection provider
|
||||
|
||||
@ProviderFor(categoryLocalDataSource)
|
||||
const categoryLocalDataSourceProvider = CategoryLocalDataSourceProvider._();
|
||||
|
||||
/// Provider for category local data source
|
||||
/// This is kept alive as it's a dependency injection provider
|
||||
|
||||
final class CategoryLocalDataSourceProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
CategoryLocalDataSource,
|
||||
CategoryLocalDataSource,
|
||||
CategoryLocalDataSource
|
||||
>
|
||||
with $Provider<CategoryLocalDataSource> {
|
||||
/// Provider for category local data source
|
||||
/// This is kept alive as it's a dependency injection provider
|
||||
const CategoryLocalDataSourceProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'categoryLocalDataSourceProvider',
|
||||
isAutoDispose: false,
|
||||
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'1f8412f2dc76a348873f1da4f76ae4a08991f269';
|
||||
@@ -0,0 +1,35 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import '../../../products/presentation/providers/products_provider.dart';
|
||||
|
||||
part 'category_product_count_provider.g.dart';
|
||||
|
||||
/// Provider that calculates product count for a specific category
|
||||
/// Uses family pattern to create a provider for each category ID
|
||||
@riverpod
|
||||
int categoryProductCount(Ref ref, String categoryId) {
|
||||
final productsAsync = ref.watch(productsProvider);
|
||||
return productsAsync.when(
|
||||
data: (products) => products.where((p) => p.categoryId == categoryId).length,
|
||||
loading: () => 0,
|
||||
error: (_, __) => 0,
|
||||
);
|
||||
}
|
||||
|
||||
/// Provider that returns all category product counts as a map
|
||||
/// Useful for displaying product counts on all category cards at once
|
||||
@riverpod
|
||||
Map<String, int> allCategoryProductCounts(Ref ref) {
|
||||
final productsAsync = ref.watch(productsProvider);
|
||||
return productsAsync.when(
|
||||
data: (products) {
|
||||
// Group products by category and count
|
||||
final counts = <String, int>{};
|
||||
for (final product in products) {
|
||||
counts[product.categoryId] = (counts[product.categoryId] ?? 0) + 1;
|
||||
}
|
||||
return counts;
|
||||
},
|
||||
loading: () => {},
|
||||
error: (_, __) => {},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'category_product_count_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Provider that calculates product count for a specific category
|
||||
/// Uses family pattern to create a provider for each category ID
|
||||
|
||||
@ProviderFor(categoryProductCount)
|
||||
const categoryProductCountProvider = CategoryProductCountFamily._();
|
||||
|
||||
/// Provider that calculates product count for a specific category
|
||||
/// Uses family pattern to create a provider for each category ID
|
||||
|
||||
final class CategoryProductCountProvider
|
||||
extends $FunctionalProvider<int, int, int>
|
||||
with $Provider<int> {
|
||||
/// Provider that calculates product count for a specific category
|
||||
/// Uses family pattern to create a provider for each category ID
|
||||
const CategoryProductCountProvider._({
|
||||
required CategoryProductCountFamily super.from,
|
||||
required String super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'categoryProductCountProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$categoryProductCountHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'categoryProductCountProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<int> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
int create(Ref ref) {
|
||||
final argument = this.argument as String;
|
||||
return categoryProductCount(ref, argument);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(int value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<int>(value),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is CategoryProductCountProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$categoryProductCountHash() =>
|
||||
r'2d51eea21a4d018964d10ee00d0957a2c38d28c6';
|
||||
|
||||
/// Provider that calculates product count for a specific category
|
||||
/// Uses family pattern to create a provider for each category ID
|
||||
|
||||
final class CategoryProductCountFamily extends $Family
|
||||
with $FunctionalFamilyOverride<int, String> {
|
||||
const CategoryProductCountFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'categoryProductCountProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
/// Provider that calculates product count for a specific category
|
||||
/// Uses family pattern to create a provider for each category ID
|
||||
|
||||
CategoryProductCountProvider call(String categoryId) =>
|
||||
CategoryProductCountProvider._(argument: categoryId, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'categoryProductCountProvider';
|
||||
}
|
||||
|
||||
/// Provider that returns all category product counts as a map
|
||||
/// Useful for displaying product counts on all category cards at once
|
||||
|
||||
@ProviderFor(allCategoryProductCounts)
|
||||
const allCategoryProductCountsProvider = AllCategoryProductCountsProvider._();
|
||||
|
||||
/// Provider that returns all category product counts as a map
|
||||
/// Useful for displaying product counts on all category cards at once
|
||||
|
||||
final class AllCategoryProductCountsProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
Map<String, int>,
|
||||
Map<String, int>,
|
||||
Map<String, int>
|
||||
>
|
||||
with $Provider<Map<String, int>> {
|
||||
/// Provider that returns all category product counts as a map
|
||||
/// Useful for displaying product counts on all category cards at once
|
||||
const AllCategoryProductCountsProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'allCategoryProductCountsProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$allCategoryProductCountsHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<Map<String, int>> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
Map<String, int> create(Ref ref) {
|
||||
return allCategoryProductCounts(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(Map<String, int> value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<Map<String, int>>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$allCategoryProductCountsHash() =>
|
||||
r'a4ecc281916772ac74327333bd76e7b6463a0992';
|
||||
@@ -0,0 +1,4 @@
|
||||
/// Export all category providers
|
||||
export 'category_datasource_provider.dart';
|
||||
export 'categories_provider.dart';
|
||||
export 'category_product_count_provider.dart';
|
||||
@@ -0,0 +1,59 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../domain/entities/category.dart';
|
||||
|
||||
/// Category card widget
|
||||
class CategoryCard extends StatelessWidget {
|
||||
final Category category;
|
||||
|
||||
const CategoryCard({
|
||||
super.key,
|
||||
required this.category,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = category.color != null
|
||||
? Color(int.parse(category.color!.substring(1), radix: 16) + 0xFF000000)
|
||||
: Theme.of(context).colorScheme.primaryContainer;
|
||||
|
||||
return Card(
|
||||
color: color,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
// TODO: Filter products by category
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.category,
|
||||
size: 48,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
category.name,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${category.productCount} products',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../providers/categories_provider.dart';
|
||||
import '../../domain/entities/category.dart';
|
||||
import 'category_card.dart';
|
||||
import '../../../../core/widgets/loading_indicator.dart';
|
||||
import '../../../../core/widgets/error_widget.dart';
|
||||
import '../../../../core/widgets/empty_state.dart';
|
||||
|
||||
/// Category grid widget
|
||||
class CategoryGrid extends ConsumerWidget {
|
||||
final void Function(Category)? onCategoryTap;
|
||||
|
||||
const CategoryGrid({
|
||||
super.key,
|
||||
this.onCategoryTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final categoriesAsync = ref.watch(categoriesProvider);
|
||||
|
||||
return categoriesAsync.when(
|
||||
loading: () => const LoadingIndicator(message: 'Loading categories...'),
|
||||
error: (error, stack) => ErrorDisplay(
|
||||
message: error.toString(),
|
||||
onRetry: () => ref.refresh(categoriesProvider),
|
||||
),
|
||||
data: (categories) {
|
||||
if (categories.isEmpty) {
|
||||
return const EmptyState(
|
||||
message: 'No categories found',
|
||||
subMessage: 'Categories will appear here once added',
|
||||
icon: Icons.category_outlined,
|
||||
);
|
||||
}
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
// Determine grid columns based on width
|
||||
int crossAxisCount = 2;
|
||||
if (constraints.maxWidth > 1200) {
|
||||
crossAxisCount = 4;
|
||||
} else if (constraints.maxWidth > 800) {
|
||||
crossAxisCount = 3;
|
||||
}
|
||||
|
||||
return GridView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: crossAxisCount,
|
||||
childAspectRatio: 1.2,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 12,
|
||||
),
|
||||
itemCount: categories.length,
|
||||
itemBuilder: (context, index) {
|
||||
final category = categories[index];
|
||||
return RepaintBoundary(
|
||||
child: GestureDetector(
|
||||
onTap: () => onCategoryTap?.call(category),
|
||||
child: CategoryCard(category: category),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Category Feature Widgets
|
||||
export 'category_card.dart';
|
||||
export 'category_grid.dart';
|
||||
|
||||
// This file provides a central export point for all category widgets
|
||||
Reference in New Issue
Block a user