import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/theme/app_colors.dart'; import '../../../shared/models/recipe.dart'; import '../recipe_provider.dart'; /// Card shown in the recommendations list. /// Shows the photo, title, nutrition summary, time and difficulty. /// The ♡ button saves / unsaves the recipe. class RecipeCard extends ConsumerWidget { final Recipe recipe; final VoidCallback onTap; const RecipeCard({ super.key, required this.recipe, required this.onTap, }); @override Widget build(BuildContext context, WidgetRef ref) { final savedNotifier = ref.watch(savedRecipesProvider.notifier); final isSaved = ref.watch( savedRecipesProvider.select( (_) => savedNotifier.isSaved(recipe.title), ), ); return Card( clipBehavior: Clip.antiAlias, child: InkWell( onTap: onTap, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Photo Stack( children: [ _RecipeImage(imageUrl: recipe.imageUrl, title: recipe.title), // Save button Positioned( top: 8, right: 8, child: _SaveButton( isSaved: isSaved, onPressed: () => _toggleSave(context, ref, isSaved), ), ), ], ), // Content Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( recipe.title, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), if (recipe.description.isNotEmpty) ...[ const SizedBox(height: 4), Text( recipe.description, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: AppColors.textSecondary, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ], const SizedBox(height: 10), _MetaRow(recipe: recipe), if (recipe.nutrition != null) ...[ const SizedBox(height: 8), _NutritionRow(nutrition: recipe.nutrition!), ], ], ), ), ], ), ), ); } Future _toggleSave( BuildContext context, WidgetRef ref, bool isSaved, ) async { HapticFeedback.lightImpact(); final notifier = ref.read(savedRecipesProvider.notifier); if (isSaved) { final id = notifier.savedId(recipe.title); if (id != null) await notifier.delete(id); } else { final saved = await notifier.save(recipe); if (saved == null && context.mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Не удалось сохранить рецепт')), ); } } } } class _RecipeImage extends StatelessWidget { final String imageUrl; final String title; const _RecipeImage({required this.imageUrl, required this.title}); @override Widget build(BuildContext context) { if (imageUrl.isEmpty) { return Container( height: 180, color: AppColors.primaryLight.withValues(alpha: 0.3), child: const Center(child: Icon(Icons.restaurant, size: 48)), ); } return CachedNetworkImage( imageUrl: imageUrl, height: 180, width: double.infinity, fit: BoxFit.cover, placeholder: (_, __) => Container( height: 180, color: Colors.grey.withValues(alpha: 0.3), ), errorWidget: (_, __, ___) => Container( height: 180, color: AppColors.primaryLight.withValues(alpha: 0.3), child: const Center(child: Icon(Icons.restaurant, size: 48)), ), ); } } class _SaveButton extends StatelessWidget { final bool isSaved; final VoidCallback onPressed; const _SaveButton({required this.isSaved, required this.onPressed}); @override Widget build(BuildContext context) { return Material( color: Colors.black45, borderRadius: BorderRadius.circular(20), child: InkWell( borderRadius: BorderRadius.circular(20), onTap: onPressed, child: Padding( padding: const EdgeInsets.all(6), child: Icon( isSaved ? Icons.favorite : Icons.favorite_border, color: isSaved ? Colors.red : Colors.white, size: 22, ), ), ), ); } } class _MetaRow extends StatelessWidget { final Recipe recipe; const _MetaRow({required this.recipe}); @override Widget build(BuildContext context) { final totalMin = recipe.prepTimeMin + recipe.cookTimeMin; final style = Theme.of(context).textTheme.bodySmall?.copyWith( color: AppColors.textSecondary, ); return Row( children: [ const Icon(Icons.access_time, size: 14, color: AppColors.textSecondary), const SizedBox(width: 3), Text('$totalMin мин', style: style), const SizedBox(width: 12), const Icon(Icons.bar_chart, size: 14, color: AppColors.textSecondary), const SizedBox(width: 3), Text(_difficultyLabel(recipe.difficulty), style: style), if (recipe.cuisine.isNotEmpty) ...[ const SizedBox(width: 12), const Icon(Icons.public, size: 14, color: AppColors.textSecondary), const SizedBox(width: 3), Text(_cuisineLabel(recipe.cuisine), style: style), ], ], ); } String _difficultyLabel(String d) => switch (d) { 'easy' => 'Легко', 'medium' => 'Средне', 'hard' => 'Сложно', _ => d, }; String _cuisineLabel(String c) => switch (c) { 'russian' => 'Русская', 'asian' => 'Азиатская', 'european' => 'Европейская', 'mediterranean' => 'Средиземноморская', 'american' => 'Американская', _ => 'Другая', }; } class _NutritionRow extends StatelessWidget { final NutritionInfo nutrition; const _NutritionRow({required this.nutrition}); @override Widget build(BuildContext context) { final style = Theme.of(context).textTheme.bodySmall?.copyWith( color: AppColors.textSecondary, fontSize: 11, ); return Row( children: [ Text('≈ ', style: style?.copyWith(color: AppColors.accent)), _NutItem(label: 'ккал', value: nutrition.calories.round(), style: style), const SizedBox(width: 8), _NutItem(label: 'б', value: nutrition.proteinG.round(), style: style), const SizedBox(width: 8), _NutItem(label: 'ж', value: nutrition.fatG.round(), style: style), const SizedBox(width: 8), _NutItem(label: 'у', value: nutrition.carbsG.round(), style: style), ], ); } } class _NutItem extends StatelessWidget { final String label; final int value; final TextStyle? style; const _NutItem({required this.label, required this.value, this.style}); @override Widget build(BuildContext context) => Text( '$value $label', style: style, ); }