news page

This commit is contained in:
Phuoc Nguyen
2025-11-03 11:48:41 +07:00
parent 21c1c3372c
commit ea485d8c3a
14 changed files with 2017 additions and 13 deletions

View File

@@ -0,0 +1,275 @@
/// News List Page
///
/// Displays all news articles with category filtering and featured section.
/// Matches HTML design at html/news-list.html
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/presentation/providers/news_provider.dart';
import 'package:worker/features/news/presentation/widgets/category_filter_chips.dart';
import 'package:worker/features/news/presentation/widgets/featured_news_card.dart';
import 'package:worker/features/news/presentation/widgets/news_card.dart';
/// News List Page
///
/// 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)
/// - 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 {
const NewsListPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Watch providers
final featuredArticleAsync = ref.watch(featuredArticleProvider);
final filteredArticlesAsync = ref.watch(filteredNewsArticlesProvider);
final selectedCategory = ref.watch(selectedNewsCategoryProvider);
return Scaffold(
backgroundColor: Colors.white,
appBar: _buildAppBar(context),
body: RefreshIndicator(
onRefresh: () async {
// Invalidate providers to trigger refresh
ref.invalidate(newsArticlesProvider);
ref.invalidate(featuredArticleProvider);
ref.invalidate(filteredNewsArticlesProvider);
},
child: CustomScrollView(
slivers: [
// Category Filter Chips
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(top: 4, bottom: AppSpacing.md),
child: CategoryFilterChips(
selectedCategory: selectedCategory,
onCategorySelected: (category) {
ref
.read(selectedNewsCategoryProvider.notifier)
.setCategory(category);
},
),
),
),
// Featured Article Section
featuredArticleAsync.when(
data: (article) {
if (article == null) {
return const SliverToBoxAdapter(child: SizedBox.shrink());
}
return SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Section title "Nổi bật"
Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
),
child: Row(
children: [
Icon(
Icons.star,
size: 18,
color: AppColors.primaryBlue,
),
const SizedBox(width: 8),
const Text(
'Nổi bật',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF1E293B),
),
),
],
),
),
const SizedBox(height: AppSpacing.md),
// Featured card
FeaturedNewsCard(
article: article,
onTap: () => _onArticleTap(context, article),
),
const SizedBox(height: 32),
],
),
);
},
loading: () => const SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.all(AppSpacing.md),
child: Center(child: CircularProgressIndicator()),
),
),
error: (error, stack) =>
const SliverToBoxAdapter(child: SizedBox.shrink()),
),
// Latest News Section
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
child: Row(
children: [
Icon(
Icons.newspaper,
size: 18,
color: AppColors.primaryBlue,
),
const SizedBox(width: 8),
const Text(
'Mới nhất',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF1E293B),
),
),
],
),
),
),
const SliverToBoxAdapter(child: SizedBox(height: AppSpacing.md)),
// News List
filteredArticlesAsync.when(
data: (articles) {
if (articles.isEmpty) {
return SliverFillRemaining(child: _buildEmptyState());
}
return SliverPadding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
),
sliver: SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
final article = articles[index];
return NewsCard(
article: article,
onTap: () => _onArticleTap(context, article),
);
}, childCount: articles.length),
),
);
},
loading: () => const SliverFillRemaining(
child: Center(child: CircularProgressIndicator()),
),
error: (error, stack) => SliverFillRemaining(
child: _buildErrorState(error.toString()),
),
),
// Bottom padding
const SliverToBoxAdapter(child: SizedBox(height: AppSpacing.md)),
],
),
),
);
}
/// Build standard AppBar
PreferredSizeWidget _buildAppBar(BuildContext context) {
return AppBar(
backgroundColor: AppColors.white,
elevation: AppBarSpecs.elevation,
title: const Text(
'Tin tức & chuyên môn',
style: TextStyle(
color: Colors.black,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
centerTitle: false,
actions: const [SizedBox(width: AppSpacing.sm)],
);
}
/// Build empty state
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.newspaper_outlined, size: 64, color: AppColors.grey500),
const SizedBox(height: 16),
const Text(
'Chưa có tin tức',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF1E293B),
),
),
const SizedBox(height: 8),
const Text(
'Hãy quay lại sau để xem các bài viết mới',
style: TextStyle(fontSize: 14, color: Color(0xFF64748B)),
textAlign: TextAlign.center,
),
],
),
);
}
/// Build error state
Widget _buildErrorState(String error) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 64, color: AppColors.danger),
const SizedBox(height: 16),
const Text(
'Không thể tải tin tức',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF1E293B),
),
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
error,
style: const TextStyle(fontSize: 14, color: Color(0xFF64748B)),
textAlign: TextAlign.center,
),
),
],
),
);
}
/// Handle article tap
void _onArticleTap(BuildContext context, NewsArticle article) {
// TODO: Navigate to article detail page when implemented
// context.push('/news/${article.id}');
// For now, show a snackbar
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Xem bài viết: ${article.title}'),
duration: const Duration(seconds: 2),
),
);
}
}