Files
mealplanner/backend/Services/TheMealDbClient.cs
Claude f58782774b feat: complete mealplanner app (backend + frontend + deployment)
.NET 8 backend with Zitadel JWT auth, TheMealDB integration,
weekly meal plan generation, shopping list aggregation.
Vue 3 + Tailwind 4 frontend with dark emerald theme,
manual OIDC PKCE auth, all views implemented.
Multi-stage Dockerfile with nginx reverse proxy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:10:10 +00:00

135 lines
4.0 KiB
C#

using System.Text.Json;
using MealPlanner.Models;
namespace MealPlanner.Services;
public class TheMealDbClient(HttpClient httpClient)
{
private const string BaseUrl = "https://www.themealdb.com/api/json/v1/1/";
public async Task<Recipe?> GetRandomAsync()
{
var response = await httpClient.GetAsync($"{BaseUrl}random.php");
if (!response.IsSuccessStatusCode) return null;
var json = await response.Content.ReadAsStringAsync();
var meals = ParseMeals(json);
return meals.FirstOrDefault();
}
public async Task<List<Recipe>> SearchAsync(string query)
{
var response = await httpClient.GetAsync($"{BaseUrl}search.php?s={Uri.EscapeDataString(query)}");
if (!response.IsSuccessStatusCode) return [];
var json = await response.Content.ReadAsStringAsync();
return ParseMeals(json);
}
public async Task<Recipe?> GetByIdAsync(string id)
{
var response = await httpClient.GetAsync($"{BaseUrl}lookup.php?i={id}");
if (!response.IsSuccessStatusCode) return null;
var json = await response.Content.ReadAsStringAsync();
return ParseMeals(json).FirstOrDefault();
}
private static List<Recipe> ParseMeals(string json)
{
var result = new List<Recipe>();
using var doc = JsonDocument.Parse(json);
if (!doc.RootElement.TryGetProperty("meals", out var mealsEl) || mealsEl.ValueKind == JsonValueKind.Null)
return result;
foreach (var meal in mealsEl.EnumerateArray())
{
var recipe = MapMeal(meal);
if (recipe is not null)
result.Add(recipe);
}
return result;
}
private static Recipe? MapMeal(JsonElement meal)
{
var id = meal.GetStringOrNull("idMeal");
var title = meal.GetStringOrNull("strMeal");
if (id is null || title is null) return null;
var recipe = new Recipe
{
Id = Guid.NewGuid(),
ExternalId = id,
Title = title,
Instructions = meal.GetStringOrNull("strInstructions") ?? "",
ImageUrl = meal.GetStringOrNull("strMealThumb"),
Source = RecipeSource.TheMealDb,
CreatedAt = DateTime.UtcNow,
};
// Parse strIngredient1-20 / strMeasure1-20
for (int i = 1; i <= 20; i++)
{
var name = meal.GetStringOrNull($"strIngredient{i}");
var measure = meal.GetStringOrNull($"strMeasure{i}");
if (string.IsNullOrWhiteSpace(name)) continue;
var ingredient = new RecipeIngredient
{
Id = Guid.NewGuid(),
RecipeId = recipe.Id,
Name = name.Trim(),
};
if (!string.IsNullOrWhiteSpace(measure))
{
ParseMeasure(measure.Trim(), out var amount, out var unit);
ingredient.Amount = amount;
ingredient.Unit = unit;
}
recipe.Ingredients.Add(ingredient);
}
return recipe;
}
private static void ParseMeasure(string measure, out decimal? amount, out string? unit)
{
amount = null;
unit = null;
if (string.IsNullOrWhiteSpace(measure)) return;
// Try to extract leading number
var parts = measure.Trim().Split(' ', 2);
if (parts.Length > 0 && decimal.TryParse(parts[0], System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out var parsed))
{
amount = parsed;
unit = parts.Length > 1 ? parts[1].Trim() : null;
}
else
{
unit = measure;
}
}
}
file static class JsonElementExtensions
{
public static string? GetStringOrNull(this JsonElement el, string propertyName)
{
if (el.TryGetProperty(propertyName, out var prop) && prop.ValueKind == JsonValueKind.String)
{
var val = prop.GetString();
return string.IsNullOrWhiteSpace(val) ? null : val;
}
return null;
}
}