add auth
This commit is contained in:
@@ -19,20 +19,36 @@ import 'package:worker/features/news/presentation/widgets/news_card.dart';
|
||||
///
|
||||
/// Features:
|
||||
/// - Standard AppBar with title "Tin tức & chuyên môn"
|
||||
/// - Horizontal scrollable category chips (Tất cả, Tin tức, Chuyên môn, Dự án, Sự kiện, Khuyến mãi)
|
||||
/// - Horizontal scrollable category chips (dynamic from Frappe API)
|
||||
/// - Featured article section (large card)
|
||||
/// - "Mới nhất" section with news cards list
|
||||
/// - RefreshIndicator for pull-to-refresh
|
||||
/// - Loading and error states
|
||||
class NewsListPage extends ConsumerWidget {
|
||||
class NewsListPage extends ConsumerStatefulWidget {
|
||||
const NewsListPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
ConsumerState<NewsListPage> createState() => _NewsListPageState();
|
||||
}
|
||||
|
||||
class _NewsListPageState extends ConsumerState<NewsListPage> {
|
||||
/// Currently selected category name (null = All)
|
||||
String? selectedCategoryName;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Watch providers
|
||||
final featuredArticleAsync = ref.watch(featuredArticleProvider);
|
||||
final filteredArticlesAsync = ref.watch(filteredNewsArticlesProvider);
|
||||
final selectedCategory = ref.watch(selectedNewsCategoryProvider);
|
||||
final newsArticlesAsync = ref.watch(newsArticlesProvider);
|
||||
|
||||
// Filter articles by selected category
|
||||
final filteredArticles = newsArticlesAsync.whenData((articles) {
|
||||
if (selectedCategoryName == null) {
|
||||
return articles;
|
||||
}
|
||||
// TODO: Filter by category when articles have category field
|
||||
return articles;
|
||||
});
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
@@ -40,9 +56,10 @@ class NewsListPage extends ConsumerWidget {
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
// Invalidate providers to trigger refresh
|
||||
ref.invalidate(newsArticlesProvider);
|
||||
ref.invalidate(featuredArticleProvider);
|
||||
ref.invalidate(filteredNewsArticlesProvider);
|
||||
ref
|
||||
..invalidate(newsArticlesProvider)
|
||||
..invalidate(featuredArticleProvider)
|
||||
..invalidate(blogCategoriesProvider);
|
||||
},
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
@@ -51,11 +68,11 @@ class NewsListPage extends ConsumerWidget {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 4, bottom: AppSpacing.md),
|
||||
child: CategoryFilterChips(
|
||||
selectedCategory: selectedCategory,
|
||||
onCategorySelected: (category) {
|
||||
ref
|
||||
.read(selectedNewsCategoryProvider.notifier)
|
||||
.setCategory(category);
|
||||
selectedCategoryName: selectedCategoryName,
|
||||
onCategorySelected: (categoryName) {
|
||||
setState(() {
|
||||
selectedCategoryName = categoryName;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -148,7 +165,7 @@ class NewsListPage extends ConsumerWidget {
|
||||
const SliverToBoxAdapter(child: SizedBox(height: AppSpacing.md)),
|
||||
|
||||
// News List
|
||||
filteredArticlesAsync.when(
|
||||
filteredArticles.when(
|
||||
data: (articles) {
|
||||
if (articles.isEmpty) {
|
||||
return SliverFillRemaining(child: _buildEmptyState());
|
||||
|
||||
@@ -5,10 +5,14 @@
|
||||
library;
|
||||
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:worker/core/network/dio_client.dart';
|
||||
import 'package:worker/core/services/frappe_auth_provider.dart';
|
||||
import 'package:worker/features/news/data/datasources/news_local_datasource.dart';
|
||||
import 'package:worker/features/news/data/repositories/news_repository_impl.dart';
|
||||
import 'package:worker/features/news/data/datasources/news_remote_datasource.dart';
|
||||
import 'package:worker/features/news/domain/entities/blog_category.dart';
|
||||
import 'package:worker/features/news/domain/entities/news_article.dart';
|
||||
import 'package:worker/features/news/domain/repositories/news_repository.dart';
|
||||
import 'package:worker/features/news/data/repositories/news_repository_impl.dart';
|
||||
|
||||
part 'news_provider.g.dart';
|
||||
|
||||
@@ -20,13 +24,27 @@ NewsLocalDataSource newsLocalDataSource(Ref ref) {
|
||||
return NewsLocalDataSource();
|
||||
}
|
||||
|
||||
/// News Remote DataSource Provider
|
||||
///
|
||||
/// Provides instance of NewsRemoteDataSource with Frappe auth service.
|
||||
@riverpod
|
||||
Future<NewsRemoteDataSource> newsRemoteDataSource(Ref ref) async {
|
||||
final dioClient = await ref.watch(dioClientProvider.future);
|
||||
final frappeAuthService = ref.watch(frappeAuthServiceProvider);
|
||||
return NewsRemoteDataSource(dioClient, frappeAuthService);
|
||||
}
|
||||
|
||||
/// News Repository Provider
|
||||
///
|
||||
/// Provides instance of NewsRepository implementation.
|
||||
@riverpod
|
||||
NewsRepository newsRepository(Ref ref) {
|
||||
Future<NewsRepository> newsRepository(Ref ref) async {
|
||||
final localDataSource = ref.watch(newsLocalDataSourceProvider);
|
||||
return NewsRepositoryImpl(localDataSource: localDataSource);
|
||||
final remoteDataSource = await ref.watch(newsRemoteDataSourceProvider.future);
|
||||
return NewsRepositoryImpl(
|
||||
localDataSource: localDataSource,
|
||||
remoteDataSource: remoteDataSource,
|
||||
);
|
||||
}
|
||||
|
||||
/// News Articles Provider
|
||||
@@ -35,7 +53,7 @@ NewsRepository newsRepository(Ref ref) {
|
||||
/// Returns AsyncValue<List<NewsArticle>> for proper loading/error handling.
|
||||
@riverpod
|
||||
Future<List<NewsArticle>> newsArticles(Ref ref) async {
|
||||
final repository = ref.watch(newsRepositoryProvider);
|
||||
final repository = await ref.watch(newsRepositoryProvider.future);
|
||||
return repository.getAllArticles();
|
||||
}
|
||||
|
||||
@@ -45,7 +63,7 @@ Future<List<NewsArticle>> newsArticles(Ref ref) async {
|
||||
/// Returns AsyncValue<NewsArticle?> (null if no featured article).
|
||||
@riverpod
|
||||
Future<NewsArticle?> featuredArticle(Ref ref) async {
|
||||
final repository = ref.watch(newsRepositoryProvider);
|
||||
final repository = await ref.watch(newsRepositoryProvider.future);
|
||||
return repository.getFeaturedArticle();
|
||||
}
|
||||
|
||||
@@ -79,7 +97,7 @@ class SelectedNewsCategory extends _$SelectedNewsCategory {
|
||||
@riverpod
|
||||
Future<List<NewsArticle>> filteredNewsArticles(Ref ref) async {
|
||||
final selectedCategory = ref.watch(selectedNewsCategoryProvider);
|
||||
final repository = ref.watch(newsRepositoryProvider);
|
||||
final repository = await ref.watch(newsRepositoryProvider.future);
|
||||
|
||||
// If no category selected, return all articles
|
||||
if (selectedCategory == null) {
|
||||
@@ -96,6 +114,22 @@ Future<List<NewsArticle>> filteredNewsArticles(Ref ref) async {
|
||||
/// Used for article detail page.
|
||||
@riverpod
|
||||
Future<NewsArticle?> newsArticleById(Ref ref, String articleId) async {
|
||||
final repository = ref.watch(newsRepositoryProvider);
|
||||
final repository = await ref.watch(newsRepositoryProvider.future);
|
||||
return repository.getArticleById(articleId);
|
||||
}
|
||||
|
||||
/// Blog Categories Provider
|
||||
///
|
||||
/// Fetches all published blog categories from Frappe API.
|
||||
/// Returns AsyncValue<List<BlogCategory>> (domain entities) for proper loading/error handling.
|
||||
///
|
||||
/// Example categories:
|
||||
/// - Tin tức (News)
|
||||
/// - Chuyên môn (Professional)
|
||||
/// - Dự án (Projects)
|
||||
/// - Khuyến mãi (Promotions)
|
||||
@riverpod
|
||||
Future<List<BlogCategory>> blogCategories(Ref ref) async {
|
||||
final repository = await ref.watch(newsRepositoryProvider.future);
|
||||
return repository.getBlogCategories();
|
||||
}
|
||||
|
||||
@@ -67,6 +67,59 @@ final class NewsLocalDataSourceProvider
|
||||
String _$newsLocalDataSourceHash() =>
|
||||
r'e7e7d71d20274fe8b498c7b15f8aeb9eb515af27';
|
||||
|
||||
/// News Remote DataSource Provider
|
||||
///
|
||||
/// Provides instance of NewsRemoteDataSource with Frappe auth service.
|
||||
|
||||
@ProviderFor(newsRemoteDataSource)
|
||||
const newsRemoteDataSourceProvider = NewsRemoteDataSourceProvider._();
|
||||
|
||||
/// News Remote DataSource Provider
|
||||
///
|
||||
/// Provides instance of NewsRemoteDataSource with Frappe auth service.
|
||||
|
||||
final class NewsRemoteDataSourceProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<NewsRemoteDataSource>,
|
||||
NewsRemoteDataSource,
|
||||
FutureOr<NewsRemoteDataSource>
|
||||
>
|
||||
with
|
||||
$FutureModifier<NewsRemoteDataSource>,
|
||||
$FutureProvider<NewsRemoteDataSource> {
|
||||
/// News Remote DataSource Provider
|
||||
///
|
||||
/// Provides instance of NewsRemoteDataSource with Frappe auth service.
|
||||
const NewsRemoteDataSourceProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'newsRemoteDataSourceProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$newsRemoteDataSourceHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<NewsRemoteDataSource> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<NewsRemoteDataSource> create(Ref ref) {
|
||||
return newsRemoteDataSource(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$newsRemoteDataSourceHash() =>
|
||||
r'27db8dc4fadf806349fe4f0ad5fed1999620c1a3';
|
||||
|
||||
/// News Repository Provider
|
||||
///
|
||||
/// Provides instance of NewsRepository implementation.
|
||||
@@ -79,8 +132,13 @@ const newsRepositoryProvider = NewsRepositoryProvider._();
|
||||
/// Provides instance of NewsRepository implementation.
|
||||
|
||||
final class NewsRepositoryProvider
|
||||
extends $FunctionalProvider<NewsRepository, NewsRepository, NewsRepository>
|
||||
with $Provider<NewsRepository> {
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<NewsRepository>,
|
||||
NewsRepository,
|
||||
FutureOr<NewsRepository>
|
||||
>
|
||||
with $FutureModifier<NewsRepository>, $FutureProvider<NewsRepository> {
|
||||
/// News Repository Provider
|
||||
///
|
||||
/// Provides instance of NewsRepository implementation.
|
||||
@@ -100,24 +158,17 @@ final class NewsRepositoryProvider
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<NewsRepository> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
$FutureProviderElement<NewsRepository> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
NewsRepository create(Ref ref) {
|
||||
FutureOr<NewsRepository> create(Ref ref) {
|
||||
return newsRepository(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(NewsRepository value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<NewsRepository>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$newsRepositoryHash() => r'1536188fae6934f147f022a8f5d7bd62ff9453b5';
|
||||
String _$newsRepositoryHash() => r'8e66d847014926ad542e402874e52d35b00cdbcc';
|
||||
|
||||
/// News Articles Provider
|
||||
///
|
||||
@@ -172,7 +223,7 @@ final class NewsArticlesProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$newsArticlesHash() => r'24d70e49f7137c614c024dc93c97451c6e161ce6';
|
||||
String _$newsArticlesHash() => r'789d916f1ce7d76f26429cfce97c65a71915edf3';
|
||||
|
||||
/// Featured Article Provider
|
||||
///
|
||||
@@ -225,7 +276,7 @@ final class FeaturedArticleProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$featuredArticleHash() => r'f7146600bc3bbaf5987ab6b09262135b1558f1c0';
|
||||
String _$featuredArticleHash() => r'5fd7057d3f828d6f717b08d59561aa9637eb0097';
|
||||
|
||||
/// Selected News Category Provider
|
||||
///
|
||||
@@ -353,7 +404,7 @@ final class FilteredNewsArticlesProvider
|
||||
}
|
||||
|
||||
String _$filteredNewsArticlesHash() =>
|
||||
r'f40a737b74b44f2d4fa86977175314ed0da471fa';
|
||||
r'f5d6faa2d510eae188f12fa41d052eeb43e08cc9';
|
||||
|
||||
/// News Article by ID Provider
|
||||
///
|
||||
@@ -424,7 +475,7 @@ final class NewsArticleByIdProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$newsArticleByIdHash() => r'4d28caa81d486fcd6cfefd16477355927bbcadc8';
|
||||
String _$newsArticleByIdHash() => r'f2b5ee4a3f7b67d0ee9e9c91169d740a9f250b50';
|
||||
|
||||
/// News Article by ID Provider
|
||||
///
|
||||
@@ -453,3 +504,76 @@ final class NewsArticleByIdFamily extends $Family
|
||||
@override
|
||||
String toString() => r'newsArticleByIdProvider';
|
||||
}
|
||||
|
||||
/// Blog Categories Provider
|
||||
///
|
||||
/// Fetches all published blog categories from Frappe API.
|
||||
/// Returns AsyncValue<List<BlogCategory>> (domain entities) for proper loading/error handling.
|
||||
///
|
||||
/// Example categories:
|
||||
/// - Tin tức (News)
|
||||
/// - Chuyên môn (Professional)
|
||||
/// - Dự án (Projects)
|
||||
/// - Khuyến mãi (Promotions)
|
||||
|
||||
@ProviderFor(blogCategories)
|
||||
const blogCategoriesProvider = BlogCategoriesProvider._();
|
||||
|
||||
/// Blog Categories Provider
|
||||
///
|
||||
/// Fetches all published blog categories from Frappe API.
|
||||
/// Returns AsyncValue<List<BlogCategory>> (domain entities) for proper loading/error handling.
|
||||
///
|
||||
/// Example categories:
|
||||
/// - Tin tức (News)
|
||||
/// - Chuyên môn (Professional)
|
||||
/// - Dự án (Projects)
|
||||
/// - Khuyến mãi (Promotions)
|
||||
|
||||
final class BlogCategoriesProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<BlogCategory>>,
|
||||
List<BlogCategory>,
|
||||
FutureOr<List<BlogCategory>>
|
||||
>
|
||||
with
|
||||
$FutureModifier<List<BlogCategory>>,
|
||||
$FutureProvider<List<BlogCategory>> {
|
||||
/// Blog Categories Provider
|
||||
///
|
||||
/// Fetches all published blog categories from Frappe API.
|
||||
/// Returns AsyncValue<List<BlogCategory>> (domain entities) for proper loading/error handling.
|
||||
///
|
||||
/// Example categories:
|
||||
/// - Tin tức (News)
|
||||
/// - Chuyên môn (Professional)
|
||||
/// - Dự án (Projects)
|
||||
/// - Khuyến mãi (Promotions)
|
||||
const BlogCategoriesProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'blogCategoriesProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$blogCategoriesHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<List<BlogCategory>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<List<BlogCategory>> create(Ref ref) {
|
||||
return blogCategories(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$blogCategoriesHash() => r'd87493142946be20ab309ea94d6173a8005b516e';
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user