import 'package:capstone_project/data/stock_list.dart'; import 'package:capstone_project/models/watched_stock.dart'; import 'package:capstone_project/providers/watchlist.dart'; import 'package:capstone_project/services/stock_price_service.dart'; import 'package:capstone_project/widgets/app_shell.dart'; import 'package:capstone_project/widgets/stock_price_chart.dart'; import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:provider/provider.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; class HomePage extends StatelessWidget { const HomePage({super.key}); static GoRoute route = GoRoute( path: "/", builder: (context, state) => const HomePage(), ); @override Widget build(BuildContext context) { return Consumer( builder: (context, watchlist, _) { final stocks = watchlist.stocks; return AugorShell( titleTag: 'Watchlist', headerLeading: [ _AppName(), ], headerTrailing: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: Button.primary( onPressed: () => showDialog( context: context, builder: (context) => const AddStockDialog(), ), child: const Text('Add stock'), ), ), ], statusLeft: StatusText('${stocks.length} stock${stocks.length == 1 ? '' : 's'} tracked'), child: stocks.isEmpty ? Center( child: SizedBox( width: 520, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('Your watchlist is empty').h2, const Gap(8), Text('Add a stock to start tracking signals and news.').muted, const Gap(16), Button.primary( onPressed: () => showDialog( context: context, builder: (context) => const AddStockDialog(), ), child: const Text('Add your first stock'), ), ], ).withPadding(all: 16), ), ) : SingleChildScrollView( child: Center( child: SizedBox( width: 1100, child: Wrap( spacing: 16, runSpacing: 16, children: [ for (final stock in stocks) SizedBox( width: 340, child: StockCard(stock: stock), ), ], ).withPadding(all: 20), ), ), ), ); }, ); } } class _AppName extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); return RichText( text: TextSpan( text: 'Augor', style: GoogleFonts.ibmPlexMono( fontSize: 13, height: 1, fontWeight: FontWeight.w700, color: theme.colorScheme.foreground, ), ), ); } } class StockCard extends StatefulWidget { final WatchedStock stock; const StockCard({super.key, required this.stock}); @override State createState() => _StockCardState(); } class _StockCardState extends State { static final StockPriceService _priceService = StockPriceService(); List _prices = []; bool _hovered = false; @override void initState() { super.initState(); _loadPrices(); } @override void didUpdateWidget(covariant StockCard oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.stock.ticker != widget.stock.ticker) { _loadPrices(); } } Future _loadPrices() async { try { final prices = await _priceService.fetchPriceHistory(widget.stock.ticker); if (!mounted) return; setState(() => _prices = prices); } catch (e) { if (!mounted) return; setState(() => _prices = []); } } @override Widget build(BuildContext context) { final theme = Theme.of(context); final stock = widget.stock; final signal = stock.latestSignal; final probability = signal == null ? 'No analysis yet' : '${(signal.impact * 100).toStringAsFixed(1)}% impact'; return MouseRegion( onEnter: (_) => setState(() => _hovered = true), onExit: (_) => setState(() => _hovered = false), cursor: SystemMouseCursors.click, child: GestureDetector( onTap: () => GoRouter.of(context).go('/stock/${stock.ticker}'), child: AnimatedContainer( duration: const Duration(milliseconds: 80), decoration: BoxDecoration( color: _hovered ? theme.colorScheme.background : theme.colorScheme.background.withValues(alpha: 0.85), border: Border.all(color: theme.colorScheme.border, width: 1), borderRadius: BorderRadius.circular(6), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: _hovered ? 0.2 : 0.12), blurRadius: 4, spreadRadius: _hovered ? 3 : 2, ), ], ), padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(stock.ticker).h2, const Gap(4), Text(stock.companyName).muted, ], ), ), _SignalBadge(direction: signal?.direction ?? 'neutral'), ], ), const Gap(16), Text(probability).semiBold, const Gap(12), StockPriceChart( prices: _prices, signals: stock.signalHistory, height: 80, compact: true, ), ], ), ), ), ); } } class _SignalBadge extends StatelessWidget { final String direction; const _SignalBadge({required this.direction}); @override Widget build(BuildContext context) { final theme = Theme.of(context); final color = switch (direction) { 'positive' => const Color(0xFF16a34a), 'negative' => const Color(0xFFdc2626), _ => theme.colorScheme.mutedForeground, }; final bg = switch (direction) { 'positive' => const Color(0xFF16a34a).withValues(alpha: 0.15), 'negative' => const Color(0xFFdc2626).withValues(alpha: 0.15), _ => theme.colorScheme.border.withValues(alpha: 0.5), }; return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: bg, borderRadius: BorderRadius.circular(4), border: Border.all(color: color.withValues(alpha: 0.3), width: 1), ), child: RichText( text: TextSpan( text: direction, style: GoogleFonts.ibmPlexMono( fontSize: 10, height: 1, fontWeight: FontWeight.w600, color: color, ), ), ), ); } } class AddStockDialog extends StatefulWidget { const AddStockDialog({super.key}); @override State createState() => _AddStockDialogState(); } class _AddStockDialogState extends State { String _query = ''; @override Widget build(BuildContext context) { final normalizedQuery = _query.trim().toLowerCase(); final results = stockList.where((stock) { if (normalizedQuery.isEmpty) return true; final ticker = (stock['ticker'] ?? '').toLowerCase(); final name = (stock['name'] ?? '').toLowerCase(); return ticker.contains(normalizedQuery) || name.contains(normalizedQuery); }).toList(); return AlertDialog( title: const Text('Add stock'), content: SizedBox( width: 420, height: 420, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ TextField( placeholder: const Text('Search ticker or company'), onChanged: (value) => setState(() => _query = value), ), const Gap(16), Expanded( child: results.isEmpty ? Center(child: Text('No matching stocks found.').muted) : ListView.separated( itemCount: results.length, separatorBuilder: (context, index) => const Divider(), itemBuilder: (context, index) { final stock = results[index]; final ticker = stock['ticker'] ?? ''; final name = stock['name'] ?? ''; final alreadyAdded = context .watch() .stocks .any((w) => w.ticker == ticker); return GestureDetector( onTap: alreadyAdded ? null : () async { final watchlist = WatchlistProvider.of(context); await watchlist.addStock(ticker, name); if (context.mounted) Navigator.of(context).pop(); }, child: Container( padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8), decoration: BoxDecoration( borderRadius: BorderRadius.circular(6), color: alreadyAdded ? Theme.of(context).colorScheme.secondary.withValues(alpha: 0.5) : null, ), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(ticker).semiBold, const Gap(2), Text(name).muted.small, ], ), ), if (alreadyAdded) const Text('Added').muted.small, ], ), ), ); }, ), ), ], ), ), ); } }