Files
food-ai/client/lib/features/recipes/recipe_provider.dart
dbastrikin e57ff8e06c feat: implement Iteration 1 — AI recipe recommendations
Backend:
- Add Groq LLM client (llama-3.3-70b) for recipe generation with JSON
  retry strategy (retries only on parse errors, not API errors)
- Add Pexels client for parallel photo search per recipe
- Add saved_recipes table (migration 004) with JSONB fields
- Add GET /recommendations endpoint (profile-aware prompt building)
- Add POST/GET/GET{id}/DELETE /saved-recipes CRUD endpoints
- Wire gemini, pexels, recommendation, savedrecipe packages in main.go

Flutter:
- Add Recipe, SavedRecipe models with json_serializable
- Add RecipeService (getRecommendations, getSavedRecipes, save, delete)
- Add RecommendationsNotifier and SavedRecipesNotifier (Riverpod)
- Add RecommendationsScreen with skeleton loading and refresh FAB
- Add RecipeDetailScreen (SliverAppBar, nutrition tooltip, steps with timer)
- Add SavedRecipesScreen with Dismissible swipe-to-delete and empty state
- Update RecipesScreen to TabBar (Recommendations / Saved)
- Add /recipe-detail route outside ShellRoute (no bottom nav)
- Extend ApiClient with getList() and deleteVoid()

Project:
- Add CLAUDE.md with English-only rule for comments and commit messages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 22:43:29 +02:00

110 lines
3.1 KiB
Dart

import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/auth/auth_provider.dart';
import '../../shared/models/recipe.dart';
import '../../shared/models/saved_recipe.dart';
import 'recipe_service.dart';
// ---------------------------------------------------------------------------
// Service provider
// ---------------------------------------------------------------------------
final recipeServiceProvider = Provider<RecipeService>((ref) {
return RecipeService(ref.read(apiClientProvider));
});
// ---------------------------------------------------------------------------
// Recommendations
// ---------------------------------------------------------------------------
class RecommendationsNotifier
extends StateNotifier<AsyncValue<List<Recipe>>> {
final RecipeService _service;
RecommendationsNotifier(this._service) : super(const AsyncValue.loading()) {
load();
}
Future<void> load({int count = 5}) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(
() => _service.getRecommendations(count: count),
);
}
}
final recommendationsProvider = StateNotifierProvider<RecommendationsNotifier,
AsyncValue<List<Recipe>>>((ref) {
return RecommendationsNotifier(ref.read(recipeServiceProvider));
});
// ---------------------------------------------------------------------------
// Saved recipes
// ---------------------------------------------------------------------------
class SavedRecipesNotifier
extends StateNotifier<AsyncValue<List<SavedRecipe>>> {
final RecipeService _service;
SavedRecipesNotifier(this._service) : super(const AsyncValue.loading()) {
load();
}
Future<void> load() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() => _service.getSavedRecipes());
}
/// Saves [recipe] and reloads the list. Returns the saved record or null on error.
Future<SavedRecipe?> save(Recipe recipe) async {
try {
final saved = await _service.saveRecipe(recipe);
await load();
return saved;
} catch (_) {
return null;
}
}
/// Removes the recipe with [id] optimistically and reverts on error.
Future<bool> delete(String id) async {
final previous = state;
state = state.whenData(
(list) => list.where((r) => r.id != id).toList(),
);
try {
await _service.deleteSavedRecipe(id);
return true;
} catch (_) {
state = previous;
return false;
}
}
/// Returns true if any saved recipe has the same title.
bool isSaved(String title) {
return state.whenOrNull(
data: (list) => list.any((r) => r.title == title),
) ??
false;
}
/// Returns the saved recipe ID for the given title, or null.
String? savedId(String title) {
return state.whenOrNull(
data: (list) {
try {
return list.firstWhere((r) => r.title == title).id;
} catch (_) {
return null;
}
},
);
}
}
final savedRecipesProvider = StateNotifierProvider<SavedRecipesNotifier,
AsyncValue<List<SavedRecipe>>>((ref) {
return SavedRecipesNotifier(ref.read(recipeServiceProvider));
});