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>
58 lines
1.7 KiB
Dart
58 lines
1.7 KiB
Dart
import 'package:dio/dio.dart';
|
|
import '../auth/secure_storage.dart';
|
|
import 'auth_interceptor.dart';
|
|
|
|
class ApiClient {
|
|
late final Dio _dio;
|
|
|
|
ApiClient({required String baseUrl, required SecureStorageService storage}) {
|
|
_dio = Dio(BaseOptions(
|
|
baseUrl: baseUrl,
|
|
connectTimeout: const Duration(seconds: 10),
|
|
receiveTimeout: const Duration(seconds: 30),
|
|
headers: {'Content-Type': 'application/json'},
|
|
));
|
|
|
|
_dio.interceptors.addAll([
|
|
AuthInterceptor(storage: storage, dio: _dio),
|
|
LogInterceptor(requestBody: true, responseBody: true),
|
|
]);
|
|
}
|
|
|
|
/// Exposed for testing only.
|
|
ApiClient.withDio(this._dio);
|
|
|
|
Future<Map<String, dynamic>> get(String path,
|
|
{Map<String, dynamic>? params}) async {
|
|
final response = await _dio.get(path, queryParameters: params);
|
|
return response.data;
|
|
}
|
|
|
|
Future<Map<String, dynamic>> post(String path, {dynamic data}) async {
|
|
final response = await _dio.post(path, data: data);
|
|
return response.data;
|
|
}
|
|
|
|
Future<Map<String, dynamic>> put(String path, {dynamic data}) async {
|
|
final response = await _dio.put(path, data: data);
|
|
return response.data;
|
|
}
|
|
|
|
Future<Map<String, dynamic>> delete(String path) async {
|
|
final response = await _dio.delete(path);
|
|
return response.data;
|
|
}
|
|
|
|
/// Returns a list for endpoints that respond with a JSON array.
|
|
Future<List<dynamic>> getList(String path,
|
|
{Map<String, dynamic>? params}) async {
|
|
final response = await _dio.get(path, queryParameters: params);
|
|
return response.data as List<dynamic>;
|
|
}
|
|
|
|
/// Deletes a resource and expects no response body (204 No Content).
|
|
Future<void> deleteVoid(String path) async {
|
|
await _dio.delete(path);
|
|
}
|
|
}
|