/// 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:worker/core/widgets/loading_indicator.dart'; import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:go_router/go_router.dart'; import 'package:share_plus/share_plus.dart'; import 'package:worker/core/constants/api_constants.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/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 colorScheme = Theme.of(context).colorScheme; final articleAsync = ref.watch(newsArticleByIdProvider(widget.articleId)); return Scaffold( backgroundColor: colorScheme.surface, appBar: _buildAppBar(context), body: articleAsync.when( data: (article) { if (article == null) { return _buildNotFoundState(context); } return _buildContent(context, article); }, loading: () => const const CustomLoadingIndicator(), error: (error, stack) => _buildErrorState(context, error.toString()), ), ); } /// Build AppBar PreferredSizeWidget _buildAppBar(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return AppBar( backgroundColor: colorScheme.surface, elevation: AppBarSpecs.elevation, title: Text( 'Chi tiết bài viết', style: TextStyle(color: colorScheme.onSurface), ), centerTitle: false, leading: IconButton( icon: FaIcon(FontAwesomeIcons.arrowLeft, color: colorScheme.onSurface, size: 20), onPressed: () => context.pop(), ), actions: [ // Share button IconButton( icon: FaIcon(FontAwesomeIcons.shareNodes, color: colorScheme.onSurface, size: 20), onPressed: _onShareTap, ), // Bookmark button IconButton( icon: FaIcon( _isBookmarked ? FontAwesomeIcons.solidBookmark : FontAwesomeIcons.bookmark, // Keep AppColors.warning for bookmarked state - semantic status color color: _isBookmarked ? AppColors.warning : colorScheme.onSurface, size: 20, ), onPressed: _onBookmarkTap, ), const SizedBox(width: AppSpacing.sm), ], ); } /// Build content Widget _buildContent(BuildContext context, NewsArticle article) { final colorScheme = Theme.of(context).colorScheme; 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: colorScheme.surfaceContainerHighest, child: const const CustomLoadingIndicator(), ), errorWidget: (context, url, error) => Container( height: 250, color: colorScheme.surfaceContainerHighest, child: FaIcon( FontAwesomeIcons.image, size: 48, color: colorScheme.onSurfaceVariant, ), ), ), // Article Content Padding( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Metadata _buildMetadata(context, article), const SizedBox(height: 16), // Title Text( article.title, style: TextStyle( fontSize: 24, fontWeight: FontWeight.w700, color: colorScheme.onSurface, height: 1.3, ), ), const SizedBox(height: 8), // Article Body - Render HTML content if (article.content != null && article.content!.isNotEmpty) Html( data: article.content, style: { "body": Style( margin: Margins.zero, padding: HtmlPaddings.zero, fontSize: FontSize(16), lineHeight: const LineHeight(1.7), color: colorScheme.onSurface, ), "h2": Style( fontSize: FontSize(20), fontWeight: FontWeight.w600, color: colorScheme.onSurface, margin: Margins.only(top: 32, bottom: 16), ), "h3": Style( fontSize: FontSize(18), fontWeight: FontWeight.w600, color: colorScheme.onSurface, margin: Margins.only(top: 24, bottom: 12), ), "p": Style( fontSize: FontSize(16), color: colorScheme.onSurface, lineHeight: const LineHeight(1.7), margin: Margins.only(bottom: 16), ), "strong": Style( fontWeight: FontWeight.w600, color: colorScheme.onSurface, ), "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: colorScheme.onSurface, lineHeight: const LineHeight(1.5), margin: Margins.only(bottom: 8), ), "blockquote": Style( backgroundColor: colorScheme.primaryContainer, border: Border( left: BorderSide(color: colorScheme.primary, 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(context, article.tags), const SizedBox(height: 32), // Social Actions _buildSocialActions(context, article), const SizedBox(height: 32), // Related Articles if (relatedArticles != null && relatedArticles.isNotEmpty) _buildRelatedArticles(context, relatedArticles), ], ), ), ], ), ); } /// Build metadata Widget _buildMetadata(BuildContext context, NewsArticle article) { final colorScheme = Theme.of(context).colorScheme; return Row( spacing: 16, children: [ // Category badge Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration( color: colorScheme.primary, borderRadius: BorderRadius.circular(16), ), child: Text( article.category.displayName, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: colorScheme.onPrimary, ), ), ), // Date _buildMetaItem(context, FontAwesomeIcons.calendar, article.formattedDate), // Reading time // _buildMetaItem(context, Icons.schedule, article.readingTimeText), // Views // _buildMetaItem( // context, // Icons.visibility, // '${article.formattedViewCount} lượt xem', // ), ], ); } /// Build metadata item Widget _buildMetaItem(BuildContext context, IconData icon, String text) { final colorScheme = Theme.of(context).colorScheme; return Row( mainAxisSize: MainAxisSize.min, children: [ FaIcon(icon, size: 12, color: colorScheme.onSurfaceVariant), const SizedBox(width: 4), Text( text, style: TextStyle(fontSize: 12, color: colorScheme.onSurfaceVariant), ), ], ); } /// Build tags section Widget _buildTagsSection(BuildContext context, List tags) { final colorScheme = Theme.of(context).colorScheme; return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: colorScheme.surfaceContainerLowest, borderRadius: BorderRadius.circular(AppRadius.lg), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Thẻ liên quan', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: colorScheme.onSurface, ), ), 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: colorScheme.surface, border: Border.all(color: colorScheme.outlineVariant), borderRadius: BorderRadius.circular(16), ), child: Text( tag, style: TextStyle( fontSize: 12, color: colorScheme.onSurfaceVariant, ), ), ), ) .toList(), ), ], ), ); } /// Build social actions section Widget _buildSocialActions(BuildContext context, NewsArticle article) { final colorScheme = Theme.of(context).colorScheme; return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( border: Border.symmetric( horizontal: BorderSide(color: colorScheme.outlineVariant), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ _buildActionButton( context, icon: _isLiked ? FontAwesomeIcons.solidHeart : FontAwesomeIcons.heart, onPressed: _onLikeTap, color: _isLiked ? Colors.red : null, ), const SizedBox(width: 8), _buildActionButton( context, icon: _isBookmarked ? FontAwesomeIcons.solidBookmark : FontAwesomeIcons.bookmark, onPressed: _onBookmarkTap, // Keep AppColors.warning for bookmarked state - semantic status color color: _isBookmarked ? AppColors.warning : null, ), const SizedBox(width: 8), _buildActionButton(context, icon: FontAwesomeIcons.shareNodes, onPressed: _onShareTap), ], ), ); } /// Build stat item Widget _buildStatItem(BuildContext context, IconData icon, String text) { final colorScheme = Theme.of(context).colorScheme; return Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 14, color: colorScheme.onSurfaceVariant), const SizedBox(width: 4), Text( text, style: TextStyle(fontSize: 14, color: colorScheme.onSurfaceVariant), ), ], ); } /// Build action button Widget _buildActionButton( BuildContext context, { required IconData icon, required VoidCallback onPressed, Color? color, }) { final colorScheme = Theme.of(context).colorScheme; return OutlinedButton( onPressed: onPressed, style: OutlinedButton.styleFrom( padding: const EdgeInsets.all(12), side: BorderSide(color: color ?? colorScheme.outlineVariant, width: 2), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), child: FaIcon(icon, size: 18, color: color ?? colorScheme.onSurfaceVariant), ); } /// Build related articles section Widget _buildRelatedArticles(BuildContext context, List articles) { final colorScheme = Theme.of(context).colorScheme; return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: colorScheme.surfaceContainerLowest, borderRadius: BorderRadius.circular(AppRadius.lg), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Bài viết liên quan', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: colorScheme.onSurface, ), ), 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(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ FaIcon(FontAwesomeIcons.fileLines, size: 64, color: colorScheme.onSurfaceVariant), const SizedBox(height: 16), Text( 'Không tìm thấy bài viết', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: colorScheme.onSurface, ), ), const SizedBox(height: 8), Text( 'Bài viết này không tồn tại hoặc đã bị xóa', style: TextStyle(fontSize: 14, color: colorScheme.onSurfaceVariant), textAlign: TextAlign.center, ), const SizedBox(height: 24), ElevatedButton( onPressed: () => context.pop(), child: const Text('Quay lại'), ), ], ), ); } /// Build error state Widget _buildErrorState(BuildContext context, String error) { final colorScheme = Theme.of(context).colorScheme; return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Keep AppColors.danger for error state - semantic status color FaIcon(FontAwesomeIcons.circleExclamation, size: 64, color: AppColors.danger), const SizedBox(height: 16), Text( 'Không thể tải bài viết', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: colorScheme.onSurface, ), ), const SizedBox(height: 8), Padding( padding: const EdgeInsets.symmetric(horizontal: 32), child: Text( error, style: TextStyle(fontSize: 14, color: colorScheme.onSurfaceVariant), 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 SharePlus.instance.share( ShareParams(text: 'Xem bài viết: ${ApiConstants.baseUrl}') ); } }