import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; import '../data/models/news_item.dart'; class RssItemMeta { final int id; final String title; final String url; final String source; final String snippet; final DateTime? publishedAt; const RssItemMeta({ required this.id, required this.title, required this.url, required this.source, required this.snippet, this.publishedAt, }); factory RssItemMeta.fromJson(Map json) => RssItemMeta( id: json['id'] as int, title: json['title'] as String? ?? '', url: json['url'] as String? ?? '', source: json['source'] as String? ?? '', snippet: json['snippet'] as String? ?? '', publishedAt: json['published_at'] != null ? DateTime.tryParse(json['published_at'] as String) : null, ); factory RssItemMeta.fromNewsItem(NewsItem item) => RssItemMeta( id: item.id, title: item.title, url: item.url, source: item.source, snippet: item.snippet, publishedAt: item.publishedAt, ); String get relativeDate { if (publishedAt == null) return ''; final diff = DateTime.now().difference(publishedAt!); if (diff.inHours < 24) return '${diff.inHours}h ago'; if (diff.inHours < 48) return 'Yesterday'; return '${publishedAt!.month}/${publishedAt!.day}'; } } class NewsCard extends StatelessWidget { final RssItemMeta item; final String? reaction; // 'up' | 'down' | null final void Function(int itemId, String reaction) onReaction; final VoidCallback? onDiscuss; final int snippetMaxLines; const NewsCard({ super.key, required this.item, required this.reaction, required this.onReaction, this.onDiscuss, this.snippetMaxLines = 2, }); Future _openUrl() async { if (item.url.isEmpty) return; final uri = Uri.tryParse(item.url); if (uri != null && await canLaunchUrl(uri)) { await launchUrl(uri, mode: LaunchMode.externalApplication); } } @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; return Card( margin: const EdgeInsets.symmetric(vertical: 4), elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), side: BorderSide(color: scheme.outlineVariant), ), child: Padding( padding: const EdgeInsets.fromLTRB(12, 10, 12, 10), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Source + date row Row( children: [ if (item.source.isNotEmpty) Text( item.source.toUpperCase(), style: textTheme.labelSmall?.copyWith( color: scheme.primary, fontWeight: FontWeight.w700, letterSpacing: 0.6, ), ), if (item.source.isNotEmpty && item.relativeDate.isNotEmpty) Padding( padding: const EdgeInsets.only(left: 8), child: Text( item.relativeDate, style: textTheme.labelSmall?.copyWith( color: scheme.onSurfaceVariant, ), ), ), ], ), const SizedBox(height: 4), // Title — tappable if URL present. Text stays in onSurface for // contrast; the primary-colored underline carries the "this is a // link" signal without tanking readability. GestureDetector( onTap: item.url.isNotEmpty ? _openUrl : null, child: Text( item.title, style: textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w600, color: scheme.onSurface, height: 1.3, decoration: item.url.isNotEmpty ? TextDecoration.underline : null, decorationColor: scheme.primary.withValues(alpha: 0.7), decorationThickness: 1.5, ), ), ), // Snippet if (item.snippet.isNotEmpty) ...[ const SizedBox(height: 4), Text( item.snippet, maxLines: snippetMaxLines, overflow: TextOverflow.ellipsis, style: textTheme.bodySmall?.copyWith( color: scheme.onSurfaceVariant, height: 1.45, ), ), ], const SizedBox(height: 8), // Actions row: reactions + discuss Row( children: [ _ReactionButton( emoji: '👍', active: reaction == 'up', onTap: () => onReaction(item.id, 'up'), ), const SizedBox(width: 6), _ReactionButton( emoji: '👎', active: reaction == 'down', onTap: () => onReaction(item.id, 'down'), ), if (onDiscuss != null) ...[ const Spacer(), _DiscussButton(onTap: onDiscuss!), ], ], ), ], ), ), ); } } class _DiscussButton extends StatelessWidget { final VoidCallback onTap; const _DiscussButton({required this.onTap}); @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; return GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3), decoration: BoxDecoration( border: Border.all(color: scheme.primary.withValues(alpha: 0.5)), borderRadius: BorderRadius.circular(6), ), child: Text( 'Discuss', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: scheme.primary, ), ), ), ); } } class _ReactionButton extends StatelessWidget { final String emoji; final bool active; final VoidCallback onTap; const _ReactionButton({ required this.emoji, required this.active, required this.onTap, }); @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; return GestureDetector( onTap: onTap, child: AnimatedContainer( duration: const Duration(milliseconds: 150), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: active ? scheme.primary.withValues(alpha: 0.12) : Colors.transparent, border: Border.all( color: active ? scheme.primary : scheme.outlineVariant, ), borderRadius: BorderRadius.circular(6), ), child: Text(emoji, style: const TextStyle(fontSize: 14)), ), ); } }