/// News Detail Page /// /// Displays full article content with images, HTML rendering, and interactions. /// Matches HTML design at html/news-detail.html library; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.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/highlight_box.dart'; import 'package:worker/features/news/presentation/widgets/related_article_card.dart'; /// News Detail Page /// /// Features: /// - AppBar with back, share, and bookmark buttons /// - Hero image (250px height) /// - Article metadata (category, date, reading time, views) /// - Title and excerpt /// - Full article body with HTML rendering /// - Tags section /// - Social engagement stats and action buttons /// - Related articles section /// - Loading and error states class NewsDetailPage extends ConsumerStatefulWidget { /// Article ID to display final String articleId; /// Constructor const NewsDetailPage({super.key, required this.articleId}); @override ConsumerState createState() => _NewsDetailPageState(); } class _NewsDetailPageState extends ConsumerState { bool _isBookmarked = false; bool _isLiked = false; @override Widget build(BuildContext context) { final articleAsync = ref.watch(newsArticleByIdProvider(widget.articleId)); return Scaffold( backgroundColor: Colors.white, appBar: _buildAppBar(context), body: articleAsync.when( data: (article) { if (article == null) { return _buildNotFoundState(); } return _buildContent(context, article); }, loading: () => const Center(child: CircularProgressIndicator()), error: (error, stack) => _buildErrorState(error.toString()), ), ); } /// Build AppBar PreferredSizeWidget _buildAppBar(BuildContext context) { return AppBar( backgroundColor: AppColors.white, elevation: AppBarSpecs.elevation, title: Text( 'Chi tiết bài viết', style: const TextStyle(color: Colors.black), ), centerTitle: false, leading: IconButton( icon: const Icon(Icons.arrow_back, color: Colors.black), onPressed: () => context.pop(), ), actions: [ // Share button IconButton( icon: const Icon(Icons.share, color: Colors.black), onPressed: _onShareTap, ), // Bookmark button IconButton( icon: Icon( _isBookmarked ? Icons.bookmark : Icons.bookmark_border, color: _isBookmarked ? AppColors.warning : Colors.black, ), onPressed: _onBookmarkTap, ), const SizedBox(width: AppSpacing.sm), ], ); } /// Build content Widget _buildContent(BuildContext context, NewsArticle article) { final relatedArticles = ref .watch(filteredNewsArticlesProvider) .value ?.where((a) => a.id != article.id && a.category == article.category) .take(3) .toList(); return SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Hero Image CachedNetworkImage( imageUrl: article.imageUrl, width: double.infinity, height: 250, fit: BoxFit.cover, placeholder: (context, url) => Container( height: 250, color: AppColors.grey100, child: const Center(child: CircularProgressIndicator()), ), errorWidget: (context, url, error) => Container( height: 250, color: AppColors.grey100, child: const Icon( Icons.image_outlined, size: 48, color: AppColors.grey500, ), ), ), // Article Content Padding( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Metadata _buildMetadata(article), const SizedBox(height: 16), // Title Text( article.title, style: const TextStyle( fontSize: 24, fontWeight: FontWeight.w700, color: Color(0xFF1E293B), height: 1.3, ), ), const SizedBox(height: 8), // Article Body - Render HTML content if (article.content != null && article.content!.isNotEmpty) Container( // Wrap Html in Container to prevent rendering issues child: Html( data: article.content, style: { "body": Style( margin: Margins.zero, padding: HtmlPaddings.zero, fontSize: FontSize(16), lineHeight: const LineHeight(1.7), color: const Color(0xFF1E293B), ), "h2": Style( fontSize: FontSize(20), fontWeight: FontWeight.w600, color: const Color(0xFF1E293B), margin: Margins.only(top: 32, bottom: 16), ), "h3": Style( fontSize: FontSize(18), fontWeight: FontWeight.w600, color: const Color(0xFF1E293B), margin: Margins.only(top: 24, bottom: 12), ), "p": Style( fontSize: FontSize(16), color: const Color(0xFF1E293B), lineHeight: const LineHeight(1.7), margin: Margins.only(bottom: 16), ), "strong": Style( fontWeight: FontWeight.w600, color: const Color(0xFF1E293B), ), "img": Style( margin: Margins.symmetric(vertical: 16), ), "ul": Style( margin: Margins.only(left: 16, bottom: 16), ), "ol": Style( margin: Margins.only(left: 16, bottom: 16), ), "li": Style( fontSize: FontSize(16), color: const Color(0xFF1E293B), lineHeight: const LineHeight(1.5), margin: Margins.only(bottom: 8), ), "blockquote": Style( backgroundColor: const Color(0xFFF0F9FF), border: const Border( left: BorderSide(color: AppColors.primaryBlue, width: 4), ), padding: HtmlPaddings.all(16), margin: Margins.symmetric(vertical: 24), fontStyle: FontStyle.italic, ), "div": Style( margin: Margins.zero, padding: HtmlPaddings.zero, ), }, onLinkTap: (url, attributes, element) { // Handle link taps if needed if (url != null) { debugPrint('Link tapped: $url'); } }, ), ), const SizedBox(height: 32), // Tags Section if (article.tags.isNotEmpty) _buildTagsSection(article.tags), const SizedBox(height: 32), // Social Actions _buildSocialActions(article), const SizedBox(height: 32), // Related Articles if (relatedArticles != null && relatedArticles.isNotEmpty) _buildRelatedArticles(relatedArticles), ], ), ), ], ), ); } /// Build metadata Widget _buildMetadata(NewsArticle article) { return Wrap( spacing: 16, runSpacing: 8, children: [ // Category badge Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration( color: AppColors.primaryBlue, borderRadius: BorderRadius.circular(16), ), child: Text( article.category.displayName, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: Colors.white, ), ), ), // Date _buildMetaItem(Icons.calendar_today, article.formattedDate), // Reading time _buildMetaItem(Icons.schedule, article.readingTimeText), // Views _buildMetaItem( Icons.visibility, '${article.formattedViewCount} lượt xem', ), ], ); } /// Build metadata item Widget _buildMetaItem(IconData icon, String text) { return Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 12, color: const Color(0xFF64748B)), const SizedBox(width: 4), Text( text, style: const TextStyle(fontSize: 12, color: Color(0xFF64748B)), ), ], ); } /// Build tags section Widget _buildTagsSection(List tags) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFFF8FAFC), borderRadius: BorderRadius.circular(AppRadius.lg), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Thẻ liên quan', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1E293B), ), ), const SizedBox(height: 12), Wrap( spacing: 8, runSpacing: 8, children: tags .map( (tag) => Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 4, ), decoration: BoxDecoration( color: Colors.white, border: Border.all(color: const Color(0xFFE2E8F0)), borderRadius: BorderRadius.circular(16), ), child: Text( tag, style: const TextStyle( fontSize: 12, color: Color(0xFF64748B), ), ), ), ) .toList(), ), ], ), ); } /// Build social actions section Widget _buildSocialActions(NewsArticle article) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( border: Border.symmetric( horizontal: BorderSide(color: const Color(0xFFE2E8F0)), ), ), child: Column( children: [ // Engagement stats Wrap( spacing: 16, runSpacing: 8, children: [ _buildStatItem(Icons.favorite, '${article.likeCount} lượt thích'), _buildStatItem( Icons.comment, '${article.commentCount} bình luận', ), _buildStatItem(Icons.share, '${article.shareCount} lượt chia sẻ'), ], ), const SizedBox(height: 16), // Action buttons Row( mainAxisAlignment: MainAxisAlignment.center, children: [ _buildActionButton( icon: _isLiked ? Icons.favorite : Icons.favorite_border, onPressed: _onLikeTap, color: _isLiked ? Colors.red : null, ), const SizedBox(width: 8), _buildActionButton( icon: _isBookmarked ? Icons.bookmark : Icons.bookmark_border, onPressed: _onBookmarkTap, color: _isBookmarked ? AppColors.warning : null, ), const SizedBox(width: 8), _buildActionButton(icon: Icons.share, onPressed: _onShareTap), ], ), ], ), ); } /// Build stat item Widget _buildStatItem(IconData icon, String text) { return Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 14, color: const Color(0xFF64748B)), const SizedBox(width: 4), Text( text, style: const TextStyle(fontSize: 14, color: Color(0xFF64748B)), ), ], ); } /// Build action button Widget _buildActionButton({ required IconData icon, required VoidCallback onPressed, Color? color, }) { return OutlinedButton( onPressed: onPressed, style: OutlinedButton.styleFrom( padding: const EdgeInsets.all(12), side: BorderSide(color: color ?? const Color(0xFFE2E8F0), width: 2), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), child: Icon(icon, size: 20, color: color ?? const Color(0xFF64748B)), ); } /// Build related articles section Widget _buildRelatedArticles(List articles) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFFF8FAFC), borderRadius: BorderRadius.circular(AppRadius.lg), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Bài viết liên quan', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1E293B), ), ), const SizedBox(height: 16), ...articles.map( (article) => RelatedArticleCard( article: article, onTap: () { // Navigate to related article context.push('/news/${article.id}'); }, ), ), ], ), ); } /// Build not found state Widget _buildNotFoundState() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.article_outlined, size: 64, color: AppColors.grey500), const SizedBox(height: 16), const Text( 'Không tìm thấy bài viết', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1E293B), ), ), const SizedBox(height: 8), const Text( 'Bài viết này không tồn tại hoặc đã bị xóa', style: TextStyle(fontSize: 14, color: Color(0xFF64748B)), textAlign: TextAlign.center, ), const SizedBox(height: 24), ElevatedButton( onPressed: () => context.pop(), child: const Text('Quay lại'), ), ], ), ); } /// 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 bài viết', 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, ), ), const SizedBox(height: 24), ElevatedButton( onPressed: () => context.pop(), child: const Text('Quay lại'), ), ], ), ); } /// Handle like tap void _onLikeTap() { setState(() { _isLiked = !_isLiked; }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( _isLiked ? 'Đã thích bài viết!' : 'Đã bỏ thích bài viết!', ), duration: const Duration(seconds: 1), ), ); } /// Handle bookmark tap void _onBookmarkTap() { setState(() { _isBookmarked = !_isBookmarked; }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( _isBookmarked ? 'Đã lưu bài viết!' : 'Đã bỏ lưu bài viết!', ), duration: const Duration(seconds: 1), ), ); } /// Handle share tap void _onShareTap() { // Copy link to clipboard Clipboard.setData( ClipboardData(text: 'https://worker.app/news/${widget.articleId}'), ); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Đã sao chép link bài viết!'), duration: Duration(seconds: 2), ), ); // TODO: Implement native share when share_plus package is added // Share.share( // 'Xem bài viết: ${article.title}\nhttps://worker.app/news/${article.id}', // subject: article.title, // ); } }