This commit is contained in:
Phuoc Nguyen
2025-11-10 14:21:27 +07:00
parent 2a71c65577
commit 36bdf6613b
33 changed files with 2206 additions and 252 deletions

View File

@@ -2,37 +2,53 @@
///
/// Horizontal scrollable list of category filter chips.
/// Used in news list page for filtering articles by category.
/// Fetches categories dynamically from the Frappe API.
library;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/theme/colors.dart';
import 'package:worker/features/news/domain/entities/news_article.dart';
import 'package:worker/features/news/domain/entities/blog_category.dart';
import 'package:worker/features/news/presentation/providers/news_provider.dart';
/// Category Filter Chips
///
/// Displays a horizontal scrollable row of filter chips for news categories.
/// Features:
/// - "Tất cả" (All) option to show all categories
/// - 5 category options: Tin tức, Chuyên môn, Dự án, Sự kiện, Khuyến mãi
/// - Dynamic categories from Frappe API (Tin tức, Chuyên môn, Dự án, Khuyến mãi)
/// - Active state styling (primary blue background, white text)
/// - Inactive state styling (grey background, grey text)
class CategoryFilterChips extends StatelessWidget {
/// Currently selected category (null = All)
final NewsCategory? selectedCategory;
/// - Loading state with shimmer effect
/// - Error state with retry button
class CategoryFilterChips extends ConsumerWidget {
/// Currently selected category name (null = All)
final String? selectedCategoryName;
/// Callback when a category is tapped
final void Function(NewsCategory? category) onCategorySelected;
/// Callback when a category is tapped (passes category name)
final void Function(String? categoryName) onCategorySelected;
/// Constructor
const CategoryFilterChips({
super.key,
required this.selectedCategory,
required this.selectedCategoryName,
required this.onCategorySelected,
});
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final categoriesAsync = ref.watch(blogCategoriesProvider);
return categoriesAsync.when(
data: (categories) => _buildCategoryChips(categories),
loading: () => _buildLoadingState(),
error: (error, stack) => _buildErrorState(error, ref),
);
}
/// Build category chips with data
Widget _buildCategoryChips(List<BlogCategory> categories) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
@@ -41,20 +57,20 @@ class CategoryFilterChips extends StatelessWidget {
// "Tất cả" chip
_buildCategoryChip(
label: 'Tất cả',
isSelected: selectedCategory == null,
isSelected: selectedCategoryName == null,
onTap: () => onCategorySelected(null),
),
const SizedBox(width: AppSpacing.sm),
// Category chips
...NewsCategory.values.map((category) {
// Dynamic category chips from API
...categories.map((category) {
return Padding(
padding: const EdgeInsets.only(right: AppSpacing.sm),
child: _buildCategoryChip(
label: category.displayName,
isSelected: selectedCategory == category,
onTap: () => onCategorySelected(category),
label: category.title,
isSelected: selectedCategoryName == category.name,
onTap: () => onCategorySelected(category.name),
),
);
}),
@@ -63,6 +79,70 @@ class CategoryFilterChips extends StatelessWidget {
);
}
/// Build loading state with shimmer placeholders
Widget _buildLoadingState() {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
child: Row(
children: List.generate(5, (index) {
return Padding(
padding: const EdgeInsets.only(right: AppSpacing.sm),
child: Container(
width: 80,
height: 32,
decoration: BoxDecoration(
color: AppColors.grey100,
borderRadius: BorderRadius.circular(24),
),
),
);
}),
),
);
}
/// Build error state with retry
Widget _buildErrorState(Object error, WidgetRef ref) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
child: Row(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
decoration: BoxDecoration(
color: AppColors.grey100,
borderRadius: BorderRadius.circular(24),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.error_outline, size: 16, color: AppColors.grey500),
const SizedBox(width: AppSpacing.xs),
Text(
'Lỗi tải danh mục',
style: TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
),
const SizedBox(width: AppSpacing.xs),
GestureDetector(
onTap: () => ref.refresh(blogCategoriesProvider),
child: Icon(Icons.refresh, size: 16, color: AppColors.primaryBlue),
),
],
),
),
],
),
);
}
/// Build individual category chip
Widget _buildCategoryChip({
required String label,